mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
Merge branch 'dev' of https://github.com/hicccc77/WeFlow into dev
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { Users, Clock, MessageSquare, Send, Inbox, Calendar, Loader2, RefreshCw, User, Medal } from 'lucide-react'
|
||||
import ReactECharts from 'echarts-for-react'
|
||||
@@ -16,7 +16,7 @@ function AnalyticsPage() {
|
||||
|
||||
const themeMode = useThemeStore((state) => state.themeMode)
|
||||
const { statistics, rankings, timeDistribution, isLoaded, setStatistics, setRankings, setTimeDistribution, markLoaded } = useAnalyticsStore()
|
||||
const loadData = async (forceRefresh = false) => {
|
||||
const loadData = useCallback(async (forceRefresh = false) => {
|
||||
if (isLoaded && !forceRefresh) return
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
@@ -55,14 +55,22 @@ function AnalyticsPage() {
|
||||
setIsLoading(false)
|
||||
if (removeListener) removeListener()
|
||||
}
|
||||
}
|
||||
}, [isLoaded, markLoaded, setRankings, setStatistics, setTimeDistribution])
|
||||
|
||||
const location = useLocation()
|
||||
|
||||
useEffect(() => {
|
||||
const force = location.state?.forceRefresh === true
|
||||
loadData(force)
|
||||
}, [location.state])
|
||||
}, [location.state, loadData])
|
||||
|
||||
useEffect(() => {
|
||||
const handleChange = () => {
|
||||
loadData(true)
|
||||
}
|
||||
window.addEventListener('wxid-changed', handleChange as EventListener)
|
||||
return () => window.removeEventListener('wxid-changed', handleChange as EventListener)
|
||||
}, [loadData])
|
||||
|
||||
const handleRefresh = () => loadData(true)
|
||||
|
||||
|
||||
@@ -245,6 +245,38 @@ function ChatPage(_props: ChatPageProps) {
|
||||
}
|
||||
}, [loadMyAvatar])
|
||||
|
||||
const handleAccountChanged = useCallback(async () => {
|
||||
senderAvatarCache.clear()
|
||||
senderAvatarLoading.clear()
|
||||
preloadImageKeysRef.current.clear()
|
||||
lastPreloadSessionRef.current = null
|
||||
setSessionDetail(null)
|
||||
setCurrentSession(null)
|
||||
setSessions([])
|
||||
setFilteredSessions([])
|
||||
setMessages([])
|
||||
setSearchKeyword('')
|
||||
setConnectionError(null)
|
||||
setConnected(false)
|
||||
setConnecting(false)
|
||||
setHasMoreMessages(true)
|
||||
setHasMoreLater(false)
|
||||
await connect()
|
||||
}, [
|
||||
connect,
|
||||
setConnected,
|
||||
setConnecting,
|
||||
setConnectionError,
|
||||
setCurrentSession,
|
||||
setFilteredSessions,
|
||||
setHasMoreLater,
|
||||
setHasMoreMessages,
|
||||
setMessages,
|
||||
setSearchKeyword,
|
||||
setSessionDetail,
|
||||
setSessions
|
||||
])
|
||||
|
||||
// 加载会话列表(优化:先返回基础数据,异步加载联系人信息)
|
||||
const loadSessions = async (options?: { silent?: boolean }) => {
|
||||
if (options?.silent) {
|
||||
@@ -842,6 +874,14 @@ function ChatPage(_props: ChatPageProps) {
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const handleChange = () => {
|
||||
void handleAccountChanged()
|
||||
}
|
||||
window.addEventListener('wxid-changed', handleChange as EventListener)
|
||||
return () => window.removeEventListener('wxid-changed', handleChange as EventListener)
|
||||
}, [handleAccountChanged])
|
||||
|
||||
useEffect(() => {
|
||||
const nextSet = new Set<string>()
|
||||
for (const msg of messages) {
|
||||
|
||||
@@ -16,6 +16,11 @@ function DataManagementPage() {
|
||||
setWxid(id)
|
||||
}
|
||||
loadConfig()
|
||||
const handleChange = () => {
|
||||
loadConfig()
|
||||
}
|
||||
window.addEventListener('wxid-changed', handleChange as EventListener)
|
||||
return () => window.removeEventListener('wxid-changed', handleChange as EventListener)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
|
||||
@@ -396,6 +396,99 @@
|
||||
}
|
||||
}
|
||||
|
||||
.select-field {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.select-trigger {
|
||||
width: 100%;
|
||||
padding: 10px 16px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 9999px;
|
||||
font-size: 14px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
&.open {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 15%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.select-value {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.select-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: color-mix(in srgb, var(--bg-primary) 85%, var(--bg-secondary));
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
padding: 6px;
|
||||
box-shadow: var(--shadow-md);
|
||||
z-index: 20;
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
backdrop-filter: blur(14px);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.select-option {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 10px 12px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: color-mix(in srgb, var(--primary) 12%, transparent);
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.option-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.option-desc {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.select-option.active .option-desc {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.media-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -1130,11 +1223,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
input:checked+.slider {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
|
||||
input:checked + .slider::before {
|
||||
input:checked+.slider::before {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { Search, Download, FolderOpen, RefreshCw, Check, Calendar, FileJson, FileText, Table, Loader2, X, ChevronDown, ChevronLeft, ChevronRight, FileSpreadsheet, Database, FileCode, CheckCircle, XCircle, ExternalLink } from 'lucide-react'
|
||||
import * as configService from '../services/config'
|
||||
import './ExportPage.scss'
|
||||
@@ -23,6 +23,7 @@ interface ExportOptions {
|
||||
exportVoiceAsText: boolean
|
||||
excelCompactColumns: boolean
|
||||
txtColumns: string[]
|
||||
displayNamePreference: 'group-nickname' | 'remark' | 'nickname'
|
||||
}
|
||||
|
||||
interface ExportResult {
|
||||
@@ -49,6 +50,8 @@ function ExportPage() {
|
||||
const [calendarDate, setCalendarDate] = useState(new Date())
|
||||
const [selectingStart, setSelectingStart] = useState(true)
|
||||
const [showMediaLayoutPrompt, setShowMediaLayoutPrompt] = useState(false)
|
||||
const [showDisplayNameSelect, setShowDisplayNameSelect] = useState(false)
|
||||
const displayNameDropdownRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [options, setOptions] = useState<ExportOptions>({
|
||||
format: 'excel',
|
||||
@@ -64,7 +67,8 @@ function ExportPage() {
|
||||
exportEmojis: true,
|
||||
exportVoiceAsText: true,
|
||||
excelCompactColumns: true,
|
||||
txtColumns: defaultTxtColumns
|
||||
txtColumns: defaultTxtColumns,
|
||||
displayNamePreference: 'remark'
|
||||
})
|
||||
|
||||
const buildDateRangeFromPreset = (preset: string) => {
|
||||
@@ -164,6 +168,19 @@ function ExportPage() {
|
||||
loadExportDefaults()
|
||||
}, [loadSessions, loadExportPath, loadExportDefaults])
|
||||
|
||||
useEffect(() => {
|
||||
const handleChange = () => {
|
||||
setSelectedSessions(new Set())
|
||||
setSearchKeyword('')
|
||||
setExportResult(null)
|
||||
setSessions([])
|
||||
setFilteredSessions([])
|
||||
loadSessions()
|
||||
}
|
||||
window.addEventListener('wxid-changed', handleChange as EventListener)
|
||||
return () => window.removeEventListener('wxid-changed', handleChange as EventListener)
|
||||
}, [loadSessions])
|
||||
|
||||
useEffect(() => {
|
||||
const removeListener = window.electronAPI.export.onProgress?.((payload) => {
|
||||
setExportProgress({
|
||||
@@ -176,6 +193,16 @@ function ExportPage() {
|
||||
removeListener?.()
|
||||
}
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const target = event.target as Node
|
||||
if (showDisplayNameSelect && displayNameDropdownRef.current && !displayNameDropdownRef.current.contains(target)) {
|
||||
setShowDisplayNameSelect(false)
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [showDisplayNameSelect])
|
||||
|
||||
useEffect(() => {
|
||||
if (!searchKeyword.trim()) {
|
||||
@@ -258,6 +285,7 @@ function ExportPage() {
|
||||
exportVoiceAsText: options.exportVoiceAsText, // 即使不导出媒体,也可以导出语音转文字内容
|
||||
excelCompactColumns: options.excelCompactColumns,
|
||||
txtColumns: options.txtColumns,
|
||||
displayNamePreference: options.displayNamePreference,
|
||||
sessionLayout,
|
||||
dateRange: options.useAllTime ? null : options.dateRange ? {
|
||||
start: Math.floor(options.dateRange.start.getTime() / 1000),
|
||||
@@ -389,6 +417,25 @@ function ExportPage() {
|
||||
{ value: 'excel', label: 'Excel', icon: FileSpreadsheet, desc: '电子表格,适合统计分析' },
|
||||
{ value: 'sql', label: 'PostgreSQL', icon: Database, desc: '数据库脚本,便于导入到数据库' }
|
||||
]
|
||||
const displayNameOptions = [
|
||||
{
|
||||
value: 'group-nickname',
|
||||
label: '群昵称优先',
|
||||
desc: '仅群聊有效,私聊显示备注/昵称'
|
||||
},
|
||||
{
|
||||
value: 'remark',
|
||||
label: '备注优先',
|
||||
desc: '有备注显示备注,否则显示昵称'
|
||||
},
|
||||
{
|
||||
value: 'nickname',
|
||||
label: '微信昵称',
|
||||
desc: '始终显示微信昵称'
|
||||
}
|
||||
]
|
||||
const displayNameOption = displayNameOptions.find(option => option.value === options.displayNamePreference)
|
||||
const displayNameLabel = displayNameOption?.label || '备注优先'
|
||||
|
||||
return (
|
||||
<div className="export-page">
|
||||
@@ -503,6 +550,44 @@ function ExportPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 发送者名称显示偏好 */}
|
||||
{(options.format === 'html' || options.format === 'json' || options.format === 'txt') && (
|
||||
<div className="setting-section">
|
||||
<h3>发送者名称显示</h3>
|
||||
<p className="setting-subtitle">选择导出时优先显示的名称</p>
|
||||
<div className="select-field" ref={displayNameDropdownRef}>
|
||||
<button
|
||||
type="button"
|
||||
className={`select-trigger ${showDisplayNameSelect ? 'open' : ''}`}
|
||||
onClick={() => setShowDisplayNameSelect(!showDisplayNameSelect)}
|
||||
>
|
||||
<span className="select-value">{displayNameLabel}</span>
|
||||
<ChevronDown size={16} />
|
||||
</button>
|
||||
{showDisplayNameSelect && (
|
||||
<div className="select-dropdown">
|
||||
{displayNameOptions.map(option => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
className={`select-option ${options.displayNamePreference === option.value ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
setOptions({
|
||||
...options,
|
||||
displayNamePreference: option.value as ExportOptions['displayNamePreference']
|
||||
})
|
||||
setShowDisplayNameSelect(false)
|
||||
}}
|
||||
>
|
||||
<span className="option-label">{option.label}</span>
|
||||
<span className="option-desc">{option.desc}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="setting-section">
|
||||
<h3>媒体文件</h3>
|
||||
<p className="setting-subtitle">导出图片/语音/表情并在记录内写入相对路径</p>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { Users, BarChart3, Clock, Image, Loader2, RefreshCw, User, Medal, Search, X, ChevronLeft, Copy, Check } from 'lucide-react'
|
||||
import { Avatar } from '../components/Avatar'
|
||||
import ReactECharts from 'echarts-for-react'
|
||||
@@ -56,7 +56,7 @@ function GroupAnalyticsPage() {
|
||||
|
||||
useEffect(() => {
|
||||
loadGroups()
|
||||
}, [])
|
||||
}, [loadGroups])
|
||||
|
||||
useEffect(() => {
|
||||
if (searchQuery) {
|
||||
@@ -93,7 +93,7 @@ function GroupAnalyticsPage() {
|
||||
}
|
||||
}, [dateRangeReady])
|
||||
|
||||
const loadGroups = async () => {
|
||||
const loadGroups = useCallback(async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const result = await window.electronAPI.groupAnalytics.getGroupChats()
|
||||
@@ -106,7 +106,23 @@ function GroupAnalyticsPage() {
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const handleChange = () => {
|
||||
setGroups([])
|
||||
setFilteredGroups([])
|
||||
setSelectedGroup(null)
|
||||
setSelectedFunction(null)
|
||||
setMembers([])
|
||||
setRankings([])
|
||||
setActiveHours({})
|
||||
setMediaStats(null)
|
||||
void loadGroups()
|
||||
}
|
||||
window.addEventListener('wxid-changed', handleChange as EventListener)
|
||||
return () => window.removeEventListener('wxid-changed', handleChange as EventListener)
|
||||
}, [loadGroups])
|
||||
|
||||
const handleGroupSelect = (group: GroupChatInfo) => {
|
||||
if (selectedGroup?.username !== group.username) {
|
||||
|
||||
@@ -1156,7 +1156,6 @@
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
padding-right: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useAppStore } from '../stores/appStore'
|
||||
import { useChatStore } from '../stores/chatStore'
|
||||
import { useThemeStore, themes } from '../stores/themeStore'
|
||||
import { useAnalyticsStore } from '../stores/analyticsStore'
|
||||
import { dialog } from '../services/ipc'
|
||||
@@ -28,7 +29,8 @@ interface WxidOption {
|
||||
}
|
||||
|
||||
function SettingsPage() {
|
||||
const { setDbConnected, setLoading, reset } = useAppStore()
|
||||
const { isDbConnected, setDbConnected, setLoading, reset } = useAppStore()
|
||||
const resetChatStore = useChatStore((state) => state.reset)
|
||||
const { currentTheme, themeMode, setTheme, setThemeMode } = useThemeStore()
|
||||
const clearAnalyticsStoreCache = useAnalyticsStore((state) => state.clearCache)
|
||||
|
||||
@@ -40,7 +42,6 @@ function SettingsPage() {
|
||||
const [wxid, setWxid] = useState('')
|
||||
const [wxidOptions, setWxidOptions] = useState<WxidOption[]>([])
|
||||
const [showWxidSelect, setShowWxidSelect] = useState(false)
|
||||
const wxidDropdownRef = useRef<HTMLDivElement>(null)
|
||||
const [showExportFormatSelect, setShowExportFormatSelect] = useState(false)
|
||||
const [showExportDateRangeSelect, setShowExportDateRangeSelect] = useState(false)
|
||||
const [showExportExcelColumnsSelect, setShowExportExcelColumnsSelect] = useState(false)
|
||||
@@ -92,9 +93,6 @@ function SettingsPage() {
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
const target = e.target as Node
|
||||
if (showWxidSelect && wxidDropdownRef.current && !wxidDropdownRef.current.contains(target)) {
|
||||
setShowWxidSelect(false)
|
||||
}
|
||||
if (showExportFormatSelect && exportFormatDropdownRef.current && !exportFormatDropdownRef.current.contains(target)) {
|
||||
setShowExportFormatSelect(false)
|
||||
}
|
||||
@@ -107,7 +105,7 @@ function SettingsPage() {
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [showWxidSelect, showExportFormatSelect, showExportDateRangeSelect, showExportExcelColumnsSelect])
|
||||
}, [showExportFormatSelect, showExportDateRangeSelect, showExportExcelColumnsSelect])
|
||||
|
||||
useEffect(() => {
|
||||
const removeDb = window.electronAPI.key.onDbKeyStatus((payload) => {
|
||||
@@ -142,14 +140,24 @@ function SettingsPage() {
|
||||
const savedExportDefaultVoiceAsText = await configService.getExportDefaultVoiceAsText()
|
||||
const savedExportDefaultExcelCompactColumns = await configService.getExportDefaultExcelCompactColumns()
|
||||
|
||||
if (savedKey) setDecryptKey(savedKey)
|
||||
if (savedPath) setDbPath(savedPath)
|
||||
if (savedWxid) setWxid(savedWxid)
|
||||
if (savedCachePath) setCachePath(savedCachePath)
|
||||
if (savedImageXorKey != null) {
|
||||
setImageXorKey(`0x${savedImageXorKey.toString(16).toUpperCase().padStart(2, '0')}`)
|
||||
|
||||
const wxidConfig = savedWxid ? await configService.getWxidConfig(savedWxid) : null
|
||||
const decryptKeyToUse = wxidConfig?.decryptKey ?? savedKey ?? ''
|
||||
const imageXorKeyToUse = typeof wxidConfig?.imageXorKey === 'number'
|
||||
? wxidConfig.imageXorKey
|
||||
: savedImageXorKey
|
||||
const imageAesKeyToUse = wxidConfig?.imageAesKey ?? savedImageAesKey ?? ''
|
||||
|
||||
setDecryptKey(decryptKeyToUse)
|
||||
if (typeof imageXorKeyToUse === 'number') {
|
||||
setImageXorKey(`0x${imageXorKeyToUse.toString(16).toUpperCase().padStart(2, '0')}`)
|
||||
} else {
|
||||
setImageXorKey('')
|
||||
}
|
||||
if (savedImageAesKey) setImageAesKey(savedImageAesKey)
|
||||
setImageAesKey(imageAesKeyToUse)
|
||||
setLogEnabled(savedLogEnabled)
|
||||
setAutoTranscribeVoice(savedAutoTranscribe)
|
||||
setTranscribeLanguages(savedTranscribeLanguages)
|
||||
@@ -255,6 +263,103 @@ function SettingsPage() {
|
||||
setTimeout(() => setMessage(null), 3000)
|
||||
}
|
||||
|
||||
type WxidKeys = {
|
||||
decryptKey: string
|
||||
imageXorKey: number | null
|
||||
imageAesKey: string
|
||||
}
|
||||
|
||||
const formatImageXorKey = (value: number) => `0x${value.toString(16).toUpperCase().padStart(2, '0')}`
|
||||
|
||||
const parseImageXorKey = (value: string) => {
|
||||
if (!value) return null
|
||||
const parsed = parseInt(value.replace(/^0x/i, ''), 16)
|
||||
return Number.isNaN(parsed) ? null : parsed
|
||||
}
|
||||
|
||||
const buildKeysFromState = (): WxidKeys => ({
|
||||
decryptKey: decryptKey || '',
|
||||
imageXorKey: parseImageXorKey(imageXorKey),
|
||||
imageAesKey: imageAesKey || ''
|
||||
})
|
||||
|
||||
const buildKeysFromConfig = (wxidConfig: configService.WxidConfig | null): WxidKeys => ({
|
||||
decryptKey: wxidConfig?.decryptKey || '',
|
||||
imageXorKey: typeof wxidConfig?.imageXorKey === 'number' ? wxidConfig.imageXorKey : null,
|
||||
imageAesKey: wxidConfig?.imageAesKey || ''
|
||||
})
|
||||
|
||||
const applyKeysToState = (keys: WxidKeys) => {
|
||||
setDecryptKey(keys.decryptKey)
|
||||
if (typeof keys.imageXorKey === 'number') {
|
||||
setImageXorKey(formatImageXorKey(keys.imageXorKey))
|
||||
} else {
|
||||
setImageXorKey('')
|
||||
}
|
||||
setImageAesKey(keys.imageAesKey)
|
||||
}
|
||||
|
||||
const syncKeysToConfig = async (keys: WxidKeys) => {
|
||||
await configService.setDecryptKey(keys.decryptKey)
|
||||
await configService.setImageXorKey(typeof keys.imageXorKey === 'number' ? keys.imageXorKey : 0)
|
||||
await configService.setImageAesKey(keys.imageAesKey)
|
||||
}
|
||||
|
||||
const applyWxidSelection = async (
|
||||
selectedWxid: string,
|
||||
options?: { preferCurrentKeys?: boolean; showToast?: boolean; toastText?: string }
|
||||
) => {
|
||||
if (!selectedWxid) return
|
||||
|
||||
const currentWxid = wxid
|
||||
const isSameWxid = currentWxid === selectedWxid
|
||||
if (currentWxid && currentWxid !== selectedWxid) {
|
||||
const currentKeys = buildKeysFromState()
|
||||
await configService.setWxidConfig(currentWxid, {
|
||||
decryptKey: currentKeys.decryptKey,
|
||||
imageXorKey: typeof currentKeys.imageXorKey === 'number' ? currentKeys.imageXorKey : 0,
|
||||
imageAesKey: currentKeys.imageAesKey
|
||||
})
|
||||
}
|
||||
|
||||
const preferCurrentKeys = options?.preferCurrentKeys ?? false
|
||||
const keys = preferCurrentKeys
|
||||
? buildKeysFromState()
|
||||
: buildKeysFromConfig(await configService.getWxidConfig(selectedWxid))
|
||||
|
||||
setWxid(selectedWxid)
|
||||
applyKeysToState(keys)
|
||||
await configService.setMyWxid(selectedWxid)
|
||||
await syncKeysToConfig(keys)
|
||||
await configService.setWxidConfig(selectedWxid, {
|
||||
decryptKey: keys.decryptKey,
|
||||
imageXorKey: typeof keys.imageXorKey === 'number' ? keys.imageXorKey : 0,
|
||||
imageAesKey: keys.imageAesKey
|
||||
})
|
||||
setShowWxidSelect(false)
|
||||
if (isDbConnected) {
|
||||
try {
|
||||
await window.electronAPI.chat.close()
|
||||
const result = await window.electronAPI.chat.connect()
|
||||
setDbConnected(result.success, dbPath || undefined)
|
||||
if (!result.success && result.error) {
|
||||
showMessage(result.error, false)
|
||||
}
|
||||
} catch (e) {
|
||||
showMessage(`切换账号后重新连接失败: ${e}`, false)
|
||||
setDbConnected(false)
|
||||
}
|
||||
}
|
||||
if (!isSameWxid) {
|
||||
clearAnalyticsStoreCache()
|
||||
resetChatStore()
|
||||
window.dispatchEvent(new CustomEvent('wxid-changed', { detail: { wxid: selectedWxid } }))
|
||||
}
|
||||
if (options?.showToast ?? true) {
|
||||
showMessage(options?.toastText || `已选择账号:${selectedWxid}`, true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAutoDetectPath = async () => {
|
||||
if (isDetectingPath) return
|
||||
setIsDetectingPath(true)
|
||||
@@ -268,11 +373,10 @@ function SettingsPage() {
|
||||
const wxids = await window.electronAPI.dbPath.scanWxids(result.path)
|
||||
setWxidOptions(wxids)
|
||||
if (wxids.length === 1) {
|
||||
setWxid(wxids[0].wxid)
|
||||
await configService.setMyWxid(wxids[0].wxid)
|
||||
showMessage(`已检测到账号:${wxids[0].wxid}`, true)
|
||||
await applyWxidSelection(wxids[0].wxid, {
|
||||
toastText: `已检测到账号:${wxids[0].wxid}`
|
||||
})
|
||||
} else if (wxids.length > 1) {
|
||||
// 多账号时弹出选择对话框
|
||||
setShowWxidSelect(true)
|
||||
}
|
||||
} else {
|
||||
@@ -297,7 +401,10 @@ function SettingsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleScanWxid = async (silent = false) => {
|
||||
const handleScanWxid = async (
|
||||
silent = false,
|
||||
options?: { preferCurrentKeys?: boolean; showDialog?: boolean }
|
||||
) => {
|
||||
if (!dbPath) {
|
||||
if (!silent) showMessage('请先选择数据库目录', false)
|
||||
return
|
||||
@@ -305,12 +412,14 @@ function SettingsPage() {
|
||||
try {
|
||||
const wxids = await window.electronAPI.dbPath.scanWxids(dbPath)
|
||||
setWxidOptions(wxids)
|
||||
const allowDialog = options?.showDialog ?? !silent
|
||||
if (wxids.length === 1) {
|
||||
setWxid(wxids[0].wxid)
|
||||
await configService.setMyWxid(wxids[0].wxid)
|
||||
if (!silent) showMessage(`已检测到账号:${wxids[0].wxid}`, true)
|
||||
} else if (wxids.length > 1) {
|
||||
// 多账号时弹出选择对话框
|
||||
await applyWxidSelection(wxids[0].wxid, {
|
||||
preferCurrentKeys: options?.preferCurrentKeys ?? false,
|
||||
showToast: !silent,
|
||||
toastText: `已检测到账号:${wxids[0].wxid}`
|
||||
})
|
||||
} else if (wxids.length > 1 && allowDialog) {
|
||||
setShowWxidSelect(true)
|
||||
} else {
|
||||
if (!silent) showMessage('未检测到账号目录,请检查路径', false)
|
||||
@@ -321,10 +430,7 @@ function SettingsPage() {
|
||||
}
|
||||
|
||||
const handleSelectWxid = async (selectedWxid: string) => {
|
||||
setWxid(selectedWxid)
|
||||
await configService.setMyWxid(selectedWxid)
|
||||
setShowWxidSelect(false)
|
||||
showMessage(`已选择账号:${selectedWxid}`, true)
|
||||
await applyWxidSelection(selectedWxid)
|
||||
}
|
||||
|
||||
const handleSelectCachePath = async () => {
|
||||
@@ -397,7 +503,7 @@ function SettingsPage() {
|
||||
setDecryptKey(result.key)
|
||||
setDbKeyStatus('密钥获取成功')
|
||||
showMessage('已自动获取解密密钥', true)
|
||||
await handleScanWxid(true)
|
||||
await handleScanWxid(true, { preferCurrentKeys: true, showDialog: false })
|
||||
} else {
|
||||
if (result.error?.includes('未找到微信安装路径') || result.error?.includes('启动微信失败')) {
|
||||
setIsManualStartPrompt(true)
|
||||
@@ -483,19 +589,14 @@ function SettingsPage() {
|
||||
await configService.setDbPath(dbPath)
|
||||
await configService.setMyWxid(wxid)
|
||||
await configService.setCachePath(cachePath)
|
||||
if (imageXorKey) {
|
||||
const parsed = parseInt(imageXorKey.replace(/^0x/i, ''), 16)
|
||||
if (!Number.isNaN(parsed)) {
|
||||
await configService.setImageXorKey(parsed)
|
||||
}
|
||||
} else {
|
||||
await configService.setImageXorKey(0)
|
||||
}
|
||||
if (imageAesKey) {
|
||||
await configService.setImageAesKey(imageAesKey)
|
||||
} else {
|
||||
await configService.setImageAesKey('')
|
||||
}
|
||||
const parsedXorKey = parseImageXorKey(imageXorKey)
|
||||
await configService.setImageXorKey(typeof parsedXorKey === 'number' ? parsedXorKey : 0)
|
||||
await configService.setImageAesKey(imageAesKey || '')
|
||||
await configService.setWxidConfig(wxid, {
|
||||
decryptKey,
|
||||
imageXorKey: typeof parsedXorKey === 'number' ? parsedXorKey : 0,
|
||||
imageAesKey
|
||||
})
|
||||
await configService.setWhisperModelDir(whisperModelDir)
|
||||
await configService.setAutoTranscribeVoice(autoTranscribeVoice)
|
||||
await configService.setTranscribeLanguages(transcribeLanguages)
|
||||
@@ -688,37 +789,13 @@ function SettingsPage() {
|
||||
<div className="form-group">
|
||||
<label>账号 wxid</label>
|
||||
<span className="form-hint">微信账号标识</span>
|
||||
<div className="wxid-input-wrapper" ref={wxidDropdownRef}>
|
||||
<div className="wxid-input-wrapper">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="例如: wxid_xxxxxx"
|
||||
value={wxid}
|
||||
onChange={(e) => setWxid(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={`wxid-dropdown-btn ${showWxidSelect ? 'open' : ''}`}
|
||||
onClick={() => wxidOptions.length > 0 ? setShowWxidSelect(!showWxidSelect) : handleScanWxid()}
|
||||
title={wxidOptions.length > 0 ? "选择已检测到的账号" : "扫描账号"}
|
||||
>
|
||||
<ChevronDown size={16} />
|
||||
</button>
|
||||
{showWxidSelect && wxidOptions.length > 0 && (
|
||||
<div className="wxid-dropdown">
|
||||
{wxidOptions.map((opt) => (
|
||||
<div
|
||||
key={opt.wxid}
|
||||
className={`wxid-option ${opt.wxid === wxid ? 'active' : ''}`}
|
||||
onClick={() => handleSelectWxid(opt.wxid)}
|
||||
>
|
||||
<span className="wxid-value">{opt.wxid}</span>
|
||||
<span className="wxid-time">
|
||||
{new Date(opt.modifiedTime).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => handleScanWxid()}><Search size={14} /> 扫描 wxid</button>
|
||||
</div>
|
||||
|
||||
@@ -194,7 +194,7 @@ export default function SnsPage() {
|
||||
}, [selectedUsernames, searchKeyword, jumpTargetDate])
|
||||
|
||||
// 获取联系人列表
|
||||
const loadContacts = async () => {
|
||||
const loadContacts = useCallback(async () => {
|
||||
setContactsLoading(true)
|
||||
try {
|
||||
const result = await window.electronAPI.chat.getSessions()
|
||||
@@ -237,7 +237,7 @@ export default function SnsPage() {
|
||||
} finally {
|
||||
setContactsLoading(false)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 初始加载
|
||||
useEffect(() => {
|
||||
@@ -255,7 +255,22 @@ export default function SnsPage() {
|
||||
};
|
||||
checkSchema();
|
||||
loadContacts()
|
||||
}, [])
|
||||
}, [loadContacts])
|
||||
|
||||
useEffect(() => {
|
||||
const handleChange = () => {
|
||||
setPosts([])
|
||||
setHasMore(true)
|
||||
setHasNewer(false)
|
||||
setSelectedUsernames([])
|
||||
setSearchKeyword('')
|
||||
setJumpTargetDate(null)
|
||||
loadContacts()
|
||||
loadPosts({ reset: true })
|
||||
}
|
||||
window.addEventListener('wxid-changed', handleChange as EventListener)
|
||||
return () => window.removeEventListener('wxid-changed', handleChange as EventListener)
|
||||
}, [loadContacts, loadPosts])
|
||||
|
||||
useEffect(() => {
|
||||
loadPosts({ reset: true })
|
||||
|
||||
@@ -269,15 +269,14 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
||||
await configService.setDecryptKey(decryptKey)
|
||||
await configService.setMyWxid(wxid)
|
||||
await configService.setCachePath(cachePath)
|
||||
if (imageXorKey) {
|
||||
const parsed = parseInt(imageXorKey.replace(/^0x/i, ''), 16)
|
||||
if (!Number.isNaN(parsed)) {
|
||||
await configService.setImageXorKey(parsed)
|
||||
}
|
||||
}
|
||||
if (imageAesKey) {
|
||||
await configService.setImageAesKey(imageAesKey)
|
||||
}
|
||||
const parsedXorKey = imageXorKey ? parseInt(imageXorKey.replace(/^0x/i, ''), 16) : null
|
||||
await configService.setImageXorKey(typeof parsedXorKey === 'number' && !Number.isNaN(parsedXorKey) ? parsedXorKey : 0)
|
||||
await configService.setImageAesKey(imageAesKey || '')
|
||||
await configService.setWxidConfig(wxid, {
|
||||
decryptKey,
|
||||
imageXorKey: typeof parsedXorKey === 'number' && !Number.isNaN(parsedXorKey) ? parsedXorKey : 0,
|
||||
imageAesKey
|
||||
})
|
||||
await configService.setOnboardingDone(true)
|
||||
|
||||
setDbConnected(true, dbPath)
|
||||
|
||||
Reference in New Issue
Block a user