mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00: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 { Worker } from 'worker_threads'
|
||||||
import { join, dirname } from 'path'
|
import { join, dirname } from 'path'
|
||||||
import { autoUpdater } from 'electron-updater'
|
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 { existsSync } from 'fs'
|
||||||
import { ConfigService } from './services/config'
|
import { ConfigService } from './services/config'
|
||||||
import { dbPathService } from './services/dbPathService'
|
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 处理器
|
// 注册 IPC 处理器
|
||||||
function registerIpcHandlers() {
|
function registerIpcHandlers() {
|
||||||
registerNotificationHandlers()
|
registerNotificationHandlers()
|
||||||
@@ -1190,6 +1249,134 @@ function registerIpcHandlers() {
|
|||||||
return true
|
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) => {
|
ipcMain.handle('chat:getSessionDetail', async (_, sessionId: string) => {
|
||||||
return chatService.getSessionDetail(sessionId)
|
return chatService.getSessionDetail(sessionId)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -166,6 +166,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
getMyAvatarUrl: () => ipcRenderer.invoke('chat:getMyAvatarUrl'),
|
getMyAvatarUrl: () => ipcRenderer.invoke('chat:getMyAvatarUrl'),
|
||||||
downloadEmoji: (cdnUrl: string, md5?: string) => ipcRenderer.invoke('chat:downloadEmoji', cdnUrl, md5),
|
downloadEmoji: (cdnUrl: string, md5?: string) => ipcRenderer.invoke('chat:downloadEmoji', cdnUrl, md5),
|
||||||
getCachedMessages: (sessionId: string) => ipcRenderer.invoke('chat:getCachedMessages', sessionId),
|
getCachedMessages: (sessionId: string) => ipcRenderer.invoke('chat:getCachedMessages', sessionId),
|
||||||
|
clearCurrentAccountData: (options: { clearCache?: boolean; clearExports?: boolean }) =>
|
||||||
|
ipcRenderer.invoke('chat:clearCurrentAccountData', options),
|
||||||
close: () => ipcRenderer.invoke('chat:close'),
|
close: () => ipcRenderer.invoke('chat:close'),
|
||||||
getSessionDetail: (sessionId: string) => ipcRenderer.invoke('chat:getSessionDetail', sessionId),
|
getSessionDetail: (sessionId: string) => ipcRenderer.invoke('chat:getSessionDetail', sessionId),
|
||||||
getSessionDetailFast: (sessionId: string) => ipcRenderer.invoke('chat:getSessionDetailFast', sessionId),
|
getSessionDetailFast: (sessionId: string) => ipcRenderer.invoke('chat:getSessionDetailFast', sessionId),
|
||||||
|
|||||||
@@ -10,8 +10,11 @@
|
|||||||
&.collapsed {
|
&.collapsed {
|
||||||
width: 64px;
|
width: 64px;
|
||||||
|
|
||||||
.sidebar-user-card {
|
.sidebar-user-card-wrap {
|
||||||
margin: 0 8px 8px;
|
margin: 0 8px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-user-card {
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
@@ -37,8 +40,39 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-user-card {
|
.sidebar-user-card-wrap {
|
||||||
|
position: relative;
|
||||||
margin: 0 12px 10px;
|
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;
|
padding: 10px;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@@ -47,6 +81,18 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
min-height: 56px;
|
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 {
|
.user-avatar {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
@@ -74,6 +120,7 @@
|
|||||||
|
|
||||||
.user-meta {
|
.user-meta {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-name {
|
.user-name {
|
||||||
@@ -93,6 +140,17 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
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 {
|
.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 {
|
[data-theme="blossom-dream"] .sidebar {
|
||||||
background: rgba(255, 255, 255, 0.6);
|
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 { 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 { useAppStore } from '../stores/appStore'
|
||||||
import * as configService from '../services/config'
|
import * as configService from '../services/config'
|
||||||
import { onExportSessionStatus, requestExportSessionStatus } from '../services/exportBridge'
|
import { onExportSessionStatus, requestExportSessionStatus } from '../services/exportBridge'
|
||||||
@@ -69,12 +69,30 @@ function Sidebar() {
|
|||||||
wxid: '',
|
wxid: '',
|
||||||
displayName: '未识别用户'
|
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)
|
const setLocked = useAppStore(state => state.setLocked)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.electronAPI.auth.verifyEnabled().then(setAuthEnabled)
|
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(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = onExportSessionStatus((payload) => {
|
const unsubscribe = onExportSessionStatus((payload) => {
|
||||||
const countFromPayload = typeof payload?.activeTaskCount === 'number'
|
const countFromPayload = typeof payload?.activeTaskCount === 'number'
|
||||||
@@ -235,6 +253,68 @@ function Sidebar() {
|
|||||||
return location.pathname === path || location.pathname.startsWith(`${path}/`)
|
return location.pathname === path || location.pathname.startsWith(`${path}/`)
|
||||||
}
|
}
|
||||||
const exportTaskBadge = activeExportTaskCount > 99 ? '99+' : `${activeExportTaskCount}`
|
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 (
|
return (
|
||||||
<aside className={`sidebar ${collapsed ? 'collapsed' : ''}`}>
|
<aside className={`sidebar ${collapsed ? 'collapsed' : ''}`}>
|
||||||
@@ -331,9 +411,29 @@ function Sidebar() {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="sidebar-footer">
|
<div className="sidebar-footer">
|
||||||
|
<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
|
<div
|
||||||
className="sidebar-user-card"
|
className={`sidebar-user-card ${isAccountMenuOpen ? 'menu-open' : ''}`}
|
||||||
title={collapsed ? `${userProfile.displayName}${userProfile.wxid ? `\n${userProfile.wxid}` : ''}` : undefined}
|
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">
|
<div className="user-avatar">
|
||||||
{userProfile.avatarUrl ? <img src={userProfile.avatarUrl} alt="" /> : <span>{getAvatarLetter(userProfile.displayName)}</span>}
|
{userProfile.avatarUrl ? <img src={userProfile.avatarUrl} alt="" /> : <span>{getAvatarLetter(userProfile.displayName)}</span>}
|
||||||
@@ -342,6 +442,12 @@ function Sidebar() {
|
|||||||
<div className="user-name">{userProfile.displayName}</div>
|
<div className="user-name">{userProfile.displayName}</div>
|
||||||
<div className="user-wxid">{userProfile.wxid || 'wxid 未识别'}</div>
|
<div className="user-wxid">{userProfile.wxid || 'wxid 未识别'}</div>
|
||||||
</div>
|
</div>
|
||||||
|
{!collapsed && (
|
||||||
|
<span className={`user-menu-caret ${isAccountMenuOpen ? 'open' : ''}`}>
|
||||||
|
<ChevronUp size={14} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{authEnabled && (
|
{authEnabled && (
|
||||||
@@ -374,6 +480,49 @@ function Sidebar() {
|
|||||||
{collapsed ? <ChevronRight size={18} /> : <ChevronLeft size={18} />}
|
{collapsed ? <ChevronRight size={18} /> : <ChevronLeft size={18} />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
</aside>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
6
src/types/electron.d.ts
vendored
6
src/types/electron.d.ts
vendored
@@ -191,6 +191,12 @@ export interface ElectronAPI {
|
|||||||
messages?: Message[]
|
messages?: Message[]
|
||||||
error?: string
|
error?: string
|
||||||
}>
|
}>
|
||||||
|
clearCurrentAccountData: (options: { clearCache?: boolean; clearExports?: boolean }) => Promise<{
|
||||||
|
success: boolean
|
||||||
|
removedPaths?: string[]
|
||||||
|
warning?: string
|
||||||
|
error?: string
|
||||||
|
}>
|
||||||
getContact: (username: string) => Promise<Contact | null>
|
getContact: (username: string) => Promise<Contact | null>
|
||||||
getContactAvatar: (username: string) => Promise<{ avatarUrl?: string; displayName?: string } | null>
|
getContactAvatar: (username: string) => Promise<{ avatarUrl?: string; displayName?: string } | null>
|
||||||
updateMessage: (sessionId: string, localId: number, createTime: number, newContent: string) => Promise<{ success: boolean; error?: string }>
|
updateMessage: (sessionId: string, localId: number, createTime: number, newContent: string) => Promise<{ success: boolean; error?: string }>
|
||||||
|
|||||||
Reference in New Issue
Block a user