mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-17 11:37:06 +08:00
feat: 设置页面新UI 未做完
This commit is contained in:
+33
-1
@@ -1,5 +1,15 @@
|
|||||||
import './preload-env'
|
import './preload-env'
|
||||||
import { app, shell, BrowserWindow, ipcMain, nativeImage, clipboard, Menu, Tray } from 'electron'
|
import {
|
||||||
|
app,
|
||||||
|
shell,
|
||||||
|
BrowserWindow,
|
||||||
|
ipcMain,
|
||||||
|
nativeImage,
|
||||||
|
clipboard,
|
||||||
|
Menu,
|
||||||
|
Tray,
|
||||||
|
dialog
|
||||||
|
} from 'electron'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
||||||
import icon from '../../resources/icon.png?asset'
|
import icon from '../../resources/icon.png?asset'
|
||||||
@@ -478,6 +488,28 @@ app.whenReady().then(async () => {
|
|||||||
return { success: true, info }
|
return { success: true, info }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('settings:selectDbRoot', async (event) => {
|
||||||
|
const result = await dialog.showOpenDialog(BrowserWindow.fromWebContents(event.sender)!, {
|
||||||
|
title: '选择微信数据库目录',
|
||||||
|
defaultPath: loadSettings().dbRoot || undefined,
|
||||||
|
properties: ['openDirectory']
|
||||||
|
})
|
||||||
|
return result.canceled ? { canceled: true } : { canceled: false, path: result.filePaths[0] }
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('settings:openAccountRoot', async () => {
|
||||||
|
const accountRoot = chat.getCurrentAccountRoot()
|
||||||
|
if (!accountRoot) return { success: false, error: '当前没有可打开的账号目录' }
|
||||||
|
const error = await shell.openPath(accountRoot)
|
||||||
|
return error ? { success: false, error } : { success: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('db:disconnect', () => {
|
||||||
|
if (!chat.isReady()) return { success: false, error: '数据库当前未连接' }
|
||||||
|
chat.setChatDb(null)
|
||||||
|
return { success: true }
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.handle('api:getStatus', () => apiServer.getState())
|
ipcMain.handle('api:getStatus', () => apiServer.getState())
|
||||||
|
|
||||||
ipcMain.handle('api:start', async (_, host?: string, port?: number) => {
|
ipcMain.handle('api:start', async (_, host?: string, port?: number) => {
|
||||||
|
|||||||
Vendored
+3
@@ -201,6 +201,9 @@ declare global {
|
|||||||
error?: string
|
error?: string
|
||||||
info?: { wxid: string; nickname: string; avatar?: string; accountRoot: string }
|
info?: { wxid: string; nickname: string; avatar?: string; accountRoot: string }
|
||||||
}>
|
}>
|
||||||
|
selectDbRoot: () => Promise<{ canceled: boolean; path?: string }>
|
||||||
|
openAccountRoot: () => Promise<{ success: boolean; error?: string }>
|
||||||
|
disconnectDb: () => Promise<{ success: boolean; error?: string }>
|
||||||
apiStatus: () => Promise<{
|
apiStatus: () => Promise<{
|
||||||
running: boolean
|
running: boolean
|
||||||
host: string
|
host: string
|
||||||
|
|||||||
@@ -75,6 +75,9 @@ const api = {
|
|||||||
testConnection: (key: string, accountRoot?: string) =>
|
testConnection: (key: string, accountRoot?: string) =>
|
||||||
ipcRenderer.invoke('db:testConnection', key, accountRoot),
|
ipcRenderer.invoke('db:testConnection', key, accountRoot),
|
||||||
reopenWithRoot: (accountRoot: string) => ipcRenderer.invoke('db:reopenWithRoot', accountRoot),
|
reopenWithRoot: (accountRoot: string) => ipcRenderer.invoke('db:reopenWithRoot', accountRoot),
|
||||||
|
selectDbRoot: () => ipcRenderer.invoke('settings:selectDbRoot'),
|
||||||
|
openAccountRoot: () => ipcRenderer.invoke('settings:openAccountRoot'),
|
||||||
|
disconnectDb: () => ipcRenderer.invoke('db:disconnect'),
|
||||||
apiStatus: () => ipcRenderer.invoke('api:getStatus'),
|
apiStatus: () => ipcRenderer.invoke('api:getStatus'),
|
||||||
apiStart: (host?: string, port?: number) => ipcRenderer.invoke('api:start', host, port),
|
apiStart: (host?: string, port?: number) => ipcRenderer.invoke('api:start', host, port),
|
||||||
apiStop: () => ipcRenderer.invoke('api:stop'),
|
apiStop: () => ipcRenderer.invoke('api:stop'),
|
||||||
|
|||||||
+45
-41
@@ -1,9 +1,10 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { Sidebar } from './components/Sidebar'
|
import { Sidebar } from './components/Sidebar'
|
||||||
import ChatWindow from './components/ChatWindow'
|
import ChatWindow from './components/ChatWindow'
|
||||||
import { SettingsPanel } from './components/SettingsPanel'
|
|
||||||
import { AppShell } from './components/layout/AppShell'
|
import { AppShell } from './components/layout/AppShell'
|
||||||
import { ApiWorkspace } from './features/api-center/ApiWorkspace'
|
import { ApiWorkspace } from './features/api-center/ApiWorkspace'
|
||||||
|
import { SettingsWorkspace } from './features/settings/SettingsWorkspace'
|
||||||
|
import type { SettingsCategoryId } from './features/settings/model/types'
|
||||||
import { AppPage } from './components/layout/navigation'
|
import { AppPage } from './components/layout/navigation'
|
||||||
import { AiReportWorkspace } from './components/reports/AiReportWorkspace'
|
import { AiReportWorkspace } from './components/reports/AiReportWorkspace'
|
||||||
import { ReportHistorySidebar } from './components/reports/ReportHistorySidebar'
|
import { ReportHistorySidebar } from './components/reports/ReportHistorySidebar'
|
||||||
@@ -149,6 +150,7 @@ const sortMessagesChronologically = (items: Message[]): Message[] =>
|
|||||||
|
|
||||||
function App(): React.ReactElement {
|
function App(): React.ReactElement {
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
||||||
|
const [isDatabaseConnected, setIsDatabaseConnected] = useState(false)
|
||||||
const [dbKey, setDbKey] = useState(import.meta.env.VITE_DB_KEY || '')
|
const [dbKey, setDbKey] = useState(import.meta.env.VITE_DB_KEY || '')
|
||||||
const [contacts, setContacts] = useState<Contact[]>([])
|
const [contacts, setContacts] = useState<Contact[]>([])
|
||||||
const [selectedContact, setSelectedContact] = useState<Contact | null>(null)
|
const [selectedContact, setSelectedContact] = useState<Contact | null>(null)
|
||||||
@@ -162,8 +164,8 @@ function App(): React.ReactElement {
|
|||||||
const [dbKeyStatusKind, setDbKeyStatusKind] = useState<'normal' | 'success' | 'error'>('normal')
|
const [dbKeyStatusKind, setDbKeyStatusKind] = useState<'normal' | 'success' | 'error'>('normal')
|
||||||
const [showDbKey, setShowDbKey] = useState(false)
|
const [showDbKey, setShowDbKey] = useState(false)
|
||||||
const [showMacKeyFaq, setShowMacKeyFaq] = useState(false)
|
const [showMacKeyFaq, setShowMacKeyFaq] = useState(false)
|
||||||
const [showSettings, setShowSettings] = useState(false)
|
|
||||||
const [activePage, setActivePage] = useState<AppPage>('archive')
|
const [activePage, setActivePage] = useState<AppPage>('archive')
|
||||||
|
const [settingsCategory, setSettingsCategory] = useState<SettingsCategoryId>('account-database')
|
||||||
const [reportSourceContact, setReportSourceContact] = useState<Contact | null>(null)
|
const [reportSourceContact, setReportSourceContact] = useState<Contact | null>(null)
|
||||||
const [reportWorkspaceView, setReportWorkspaceView] = useState<ReportWorkspaceView>('result')
|
const [reportWorkspaceView, setReportWorkspaceView] = useState<ReportWorkspaceView>('result')
|
||||||
const [generatedReports, setGeneratedReports] = useState<GeneratedReportRecord[]>([])
|
const [generatedReports, setGeneratedReports] = useState<GeneratedReportRecord[]>([])
|
||||||
@@ -173,7 +175,7 @@ function App(): React.ReactElement {
|
|||||||
const [reportNotice, setReportNotice] = useState('')
|
const [reportNotice, setReportNotice] = useState('')
|
||||||
const [summaryDateRange, setSummaryDateRange] = useState<SummaryDateRange>('today')
|
const [summaryDateRange, setSummaryDateRange] = useState<SummaryDateRange>('today')
|
||||||
const [summaryMessageTypes, setSummaryMessageTypes] = useState<SummaryMessageType[]>(['text'])
|
const [summaryMessageTypes, setSummaryMessageTypes] = useState<SummaryMessageType[]>(['text'])
|
||||||
const [aiModelConfig, setAiModelConfig] = useState<AiModelConfig>(() => ({
|
const [aiModelConfig] = useState<AiModelConfig>(() => ({
|
||||||
apiKey: localStorage.getItem('ai_api_key') || '',
|
apiKey: localStorage.getItem('ai_api_key') || '',
|
||||||
baseURL: localStorage.getItem('ai_base_url') || 'https://api.deepseek.com',
|
baseURL: localStorage.getItem('ai_base_url') || 'https://api.deepseek.com',
|
||||||
model: localStorage.getItem('ai_model') || 'deepseek-chat'
|
model: localStorage.getItem('ai_model') || 'deepseek-chat'
|
||||||
@@ -367,6 +369,7 @@ function App(): React.ReactElement {
|
|||||||
if (success) {
|
if (success) {
|
||||||
setIsNativeMonitorActive(typeof result !== 'boolean' && result.monitoring === true)
|
setIsNativeMonitorActive(typeof result !== 'boolean' && result.monitoring === true)
|
||||||
setIsAuthenticated(true)
|
setIsAuthenticated(true)
|
||||||
|
setIsDatabaseConnected(true)
|
||||||
setDbKeyStatus('已自动连接')
|
setDbKeyStatus('已自动连接')
|
||||||
setDbKeyStatusKind('success')
|
setDbKeyStatusKind('success')
|
||||||
await loadContacts()
|
await loadContacts()
|
||||||
@@ -448,6 +451,7 @@ function App(): React.ReactElement {
|
|||||||
percent: 100
|
percent: 100
|
||||||
})
|
})
|
||||||
setIsAuthenticated(true)
|
setIsAuthenticated(true)
|
||||||
|
setIsDatabaseConnected(true)
|
||||||
setBootState('login')
|
setBootState('login')
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
setStartupProgress(null)
|
setStartupProgress(null)
|
||||||
@@ -791,6 +795,7 @@ function App(): React.ReactElement {
|
|||||||
|
|
||||||
const handlePageChange = (page: AppPage): void => {
|
const handlePageChange = (page: AppPage): void => {
|
||||||
setActivePage(page)
|
setActivePage(page)
|
||||||
|
if (page === 'settings') setSettingsCategory('account-database')
|
||||||
if (page === 'report' && isGroupContact(selectedContact) && !reportSourceContact) {
|
if (page === 'report' && isGroupContact(selectedContact) && !reportSourceContact) {
|
||||||
setReportSourceContact(selectedContact)
|
setReportSourceContact(selectedContact)
|
||||||
}
|
}
|
||||||
@@ -800,6 +805,17 @@ function App(): React.ReactElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openSettings = (): void => {
|
||||||
|
setSettingsCategory('account-database')
|
||||||
|
setActivePage('settings')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSettingsConnectionChanged = (): void => {
|
||||||
|
void refreshSelfInfo()
|
||||||
|
void loadContacts()
|
||||||
|
void window.api.getSelf().then((result) => setIsDatabaseConnected(result.ready))
|
||||||
|
}
|
||||||
|
|
||||||
const openReport = (reportId: string): void => {
|
const openReport = (reportId: string): void => {
|
||||||
setSelectedReportId(reportId)
|
setSelectedReportId(reportId)
|
||||||
setReportWorkspaceView('result')
|
setReportWorkspaceView('result')
|
||||||
@@ -830,12 +846,6 @@ function App(): React.ReactElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSaveAiModelConfig = (): void => {
|
|
||||||
localStorage.setItem('ai_api_key', aiModelConfig.apiKey)
|
|
||||||
localStorage.setItem('ai_base_url', aiModelConfig.baseURL)
|
|
||||||
localStorage.setItem('ai_model', aiModelConfig.model)
|
|
||||||
}
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (
|
if (
|
||||||
reportGeneration.phase !== 'success' ||
|
reportGeneration.phase !== 'success' ||
|
||||||
@@ -989,8 +999,8 @@ function App(): React.ReactElement {
|
|||||||
dateRange={dateRange}
|
dateRange={dateRange}
|
||||||
onDateRangeChange={handleDateRangeChange}
|
onDateRangeChange={handleDateRangeChange}
|
||||||
selfInfo={selfInfo}
|
selfInfo={selfInfo}
|
||||||
dbReady={isAuthenticated}
|
dbReady={isDatabaseConnected}
|
||||||
onOpenSettings={() => setShowSettings(true)}
|
onOpenSettings={openSettings}
|
||||||
/>
|
/>
|
||||||
<div className="resizer" onMouseDown={startResizing} />
|
<div className="resizer" onMouseDown={startResizing} />
|
||||||
<ChatWindow
|
<ChatWindow
|
||||||
@@ -1016,11 +1026,11 @@ function App(): React.ReactElement {
|
|||||||
reports={generatedReports}
|
reports={generatedReports}
|
||||||
selectedReportId={selectedReportId}
|
selectedReportId={selectedReportId}
|
||||||
selfInfo={selfInfo}
|
selfInfo={selfInfo}
|
||||||
dbReady={isAuthenticated}
|
dbReady={isDatabaseConnected}
|
||||||
onSelectReport={openReport}
|
onSelectReport={openReport}
|
||||||
onCreateReport={openReportConfigure}
|
onCreateReport={openReportConfigure}
|
||||||
onDeleteReport={handleDeleteReport}
|
onDeleteReport={handleDeleteReport}
|
||||||
onOpenSettings={() => setShowSettings(true)}
|
onOpenSettings={openSettings}
|
||||||
/>
|
/>
|
||||||
<ReportViewer
|
<ReportViewer
|
||||||
report={selectedReport}
|
report={selectedReport}
|
||||||
@@ -1038,9 +1048,9 @@ function App(): React.ReactElement {
|
|||||||
contacts={contacts}
|
contacts={contacts}
|
||||||
selectedContact={reportSourceContact}
|
selectedContact={reportSourceContact}
|
||||||
selfInfo={selfInfo}
|
selfInfo={selfInfo}
|
||||||
dbReady={isAuthenticated}
|
dbReady={isDatabaseConnected}
|
||||||
onSelectContact={handleSelectReportSource}
|
onSelectContact={handleSelectReportSource}
|
||||||
onOpenSettings={() => setShowSettings(true)}
|
onOpenSettings={openSettings}
|
||||||
/>
|
/>
|
||||||
<AiReportWorkspace
|
<AiReportWorkspace
|
||||||
sourceContact={reportSourceContact}
|
sourceContact={reportSourceContact}
|
||||||
@@ -1058,7 +1068,7 @@ function App(): React.ReactElement {
|
|||||||
isGenerating={reportGeneration.isGenerating}
|
isGenerating={reportGeneration.isGenerating}
|
||||||
onSummaryDateRangeChange={setSummaryDateRange}
|
onSummaryDateRangeChange={setSummaryDateRange}
|
||||||
onSummaryMessageTypesChange={setSummaryMessageTypes}
|
onSummaryMessageTypesChange={setSummaryMessageTypes}
|
||||||
onOpenModelSettings={() => setShowSettings(true)}
|
onOpenModelSettings={openSettings}
|
||||||
onGenerate={() => {
|
onGenerate={() => {
|
||||||
reportGeneration.resetGenerationStatus()
|
reportGeneration.resetGenerationStatus()
|
||||||
void reportGeneration.generate()
|
void reportGeneration.generate()
|
||||||
@@ -1090,16 +1100,28 @@ function App(): React.ReactElement {
|
|||||||
return (
|
return (
|
||||||
<ApiWorkspace
|
<ApiWorkspace
|
||||||
selectedContact={selectedContact}
|
selectedContact={selectedContact}
|
||||||
dbReady={isAuthenticated}
|
dbReady={isDatabaseConnected}
|
||||||
onOpenSettings={() => {
|
onOpenSettings={openSettings}
|
||||||
setActivePage('settings')
|
/>
|
||||||
setShowSettings(true)
|
)
|
||||||
|
case 'settings':
|
||||||
|
return (
|
||||||
|
<SettingsWorkspace
|
||||||
|
selectedCategory={settingsCategory}
|
||||||
|
onCategoryChange={setSettingsCategory}
|
||||||
|
selfInfo={selfInfo}
|
||||||
|
dbReady={isDatabaseConnected}
|
||||||
|
dbKey={dbKey}
|
||||||
|
onConnectionChanged={handleSettingsConnectionChanged}
|
||||||
|
onNotice={(message) => {
|
||||||
|
setReportNotice(message)
|
||||||
|
window.setTimeout(() => setReportNotice(''), 3200)
|
||||||
}}
|
}}
|
||||||
|
onOpenSettings={openSettings}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case 'search':
|
case 'search':
|
||||||
case 'export':
|
case 'export':
|
||||||
case 'settings':
|
|
||||||
return renderPlaceholderPage(activePage)
|
return renderPlaceholderPage(activePage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1234,30 +1256,12 @@ function App(): React.ReactElement {
|
|||||||
<AppShell
|
<AppShell
|
||||||
activePage={activePage}
|
activePage={activePage}
|
||||||
selfInfo={selfInfo}
|
selfInfo={selfInfo}
|
||||||
dbReady={isAuthenticated}
|
dbReady={isDatabaseConnected}
|
||||||
onPageChange={handlePageChange}
|
onPageChange={handlePageChange}
|
||||||
onOpenSettings={() => {
|
onOpenSettings={openSettings}
|
||||||
setActivePage('settings')
|
|
||||||
setShowSettings(true)
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{reportNotice && <div className="app-toast">{reportNotice}</div>}
|
{reportNotice && <div className="app-toast">{reportNotice}</div>}
|
||||||
{renderCurrentWorkspace()}
|
{renderCurrentWorkspace()}
|
||||||
<SettingsPanel
|
|
||||||
open={showSettings}
|
|
||||||
selfInfo={selfInfo}
|
|
||||||
dbReady={isAuthenticated}
|
|
||||||
dbKey={dbKey}
|
|
||||||
aiModelConfig={aiModelConfig}
|
|
||||||
onClose={() => setShowSettings(false)}
|
|
||||||
onDbKeyChange={setDbKey}
|
|
||||||
onAiModelConfigChange={setAiModelConfig}
|
|
||||||
onSaveAiModelConfig={handleSaveAiModelConfig}
|
|
||||||
onDbRootChanged={() => {
|
|
||||||
void refreshSelfInfo()
|
|
||||||
void loadContacts()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</AppShell>
|
</AppShell>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3548,6 +3548,15 @@ body {
|
|||||||
|
|
||||||
/* API-01 本地 API 中心 */
|
/* API-01 本地 API 中心 */
|
||||||
.api-center-layout { display: grid; grid-template-columns: minmax(0, 1fr) 320px; width: 100%; height: 100%; min-width: 0; min-height: 0; overflow: hidden; background: var(--wxex-bg-main); }
|
.api-center-layout { display: grid; grid-template-columns: minmax(0, 1fr) 320px; width: 100%; height: 100%; min-width: 0; min-height: 0; overflow: hidden; background: var(--wxex-bg-main); }
|
||||||
|
|
||||||
|
/* SETTINGS-01: formal settings workspace. The legacy SettingsPanel remains only as a fallback source. */
|
||||||
|
.settings-workspace { display:flex; width:100%; height:100%; min-width:0; min-height:0; overflow:hidden; background:#fafbfa; }
|
||||||
|
.settings-sidebar { width:294px; flex:0 0 294px; min-width:0; min-height:0; display:flex; flex-direction:column; background:#eef2f0; border-right:1px solid #dde3e0; }
|
||||||
|
.settings-sidebar header { padding:22px 20px 16px; border-bottom:1px solid #dde3e0; }.settings-sidebar h1 { margin:0; color:#202724; font-size:20px; }.settings-sidebar header p { margin:5px 0 16px; color:#66706b; font-size:12px; }.settings-search { height:34px; display:flex; align-items:center; gap:8px; padding:0 10px; border:1px solid #dde3e0; border-radius:8px; background:#fff; color:#66706b; }.settings-search input { min-width:0; width:100%; border:0; outline:0; color:#202724; background:transparent; font:inherit; font-size:12px; }
|
||||||
|
.settings-sidebar-list { flex:1; min-height:0; overflow:auto; padding:12px 8px; }.settings-sidebar-list section { margin:0 0 18px; }.settings-sidebar-list h2 { margin:0 12px 6px; color:#929a96; font-size:12px; font-weight:500; }.settings-sidebar-list button { position:relative; display:block; width:100%; border:0; padding:9px 12px; background:transparent; color:#39433e; text-align:left; font:600 13px inherit; cursor:pointer; border-radius:8px; }.settings-sidebar-list button.active { background:#fff; color:#202724; }.settings-sidebar-list button.active::before { content:''; position:absolute; left:0; top:7px; bottom:7px; width:3px; border-radius:0 3px 3px 0; background:#247a63; }.settings-sidebar-account { flex:0 0 auto; padding:10px; border-top:1px solid #dde3e0; }
|
||||||
|
.settings-page { position:relative; display:flex; flex:1; min-width:0; min-height:0; flex-direction:column; }.settings-page-header { flex:0 0 auto; display:flex; justify-content:space-between; align-items:flex-start; padding:24px 34px; border-bottom:1px solid #eef1ef; background:#fafbfa; }.settings-page-header h1 { margin:0; color:#202724; font-size:21px; }.settings-page-header p { margin:6px 0 0; color:#66706b; font-size:13px; }.settings-status-badge { margin-top:5px; border-radius:16px; padding:7px 12px; background:#edf5f1; color:#2e8b68; font-size:12px; }.settings-status-badge::before { content:'●'; margin-right:6px; font-size:9px; }.settings-status-badge.error { background:#f9eeee; color:#c85a5a; }.settings-status-badge.unavailable { background:#f1f3f2; color:#66706b; }
|
||||||
|
.settings-page-scroll { flex:1; min-height:0; overflow-y:auto; overflow-x:hidden; }.settings-page-content { max-width:820px; padding:34px 34px 64px; }.settings-privacy-notice { display:flex; gap:14px; align-items:flex-start; padding:17px; border:1px solid #bfded3; border-radius:10px; background:#f0f7f4; color:#36564d; }.settings-privacy-notice svg { flex:0 0 24px; width:24px; stroke:#247a63; fill:none; stroke-width:1.8; }.settings-privacy-notice strong { color:#294b41; font-size:14px; }.settings-privacy-notice p { margin:5px 0 0; color:#66706b; font-size:13px; line-height:1.6; }.settings-section-heading { margin:30px 0 14px; color:#3d4742; font-size:16px; }.settings-section-heading.danger { color:#c85a5a; }.settings-card { border:1px solid #dde3e0; border-radius:10px; background:#fff; padding:24px 26px; }.settings-account-overview { display:grid; grid-template-columns:66px minmax(120px,1fr) minmax(120px,1fr) auto; gap:24px; align-items:center; }.settings-avatar { width:66px; height:66px; overflow:hidden; display:grid; place-items:center; border-radius:9px; background:#dcede6; color:#247a63; font-size:23px; }.settings-avatar img { width:100%; height:100%; object-fit:cover; }.settings-account-meta { display:grid; gap:4px; min-width:0; }.settings-account-meta span,.settings-field-label { color:#929a96; font-size:12px; }.settings-account-meta strong { overflow:hidden; color:#35403b; font-size:13px; text-overflow:ellipsis; white-space:nowrap; }.success-text { color:#2e8b68 !important; }.settings-account-actions { display:grid; gap:8px; }.settings-path-row { display:flex; gap:10px; align-items:center; margin-top:10px; }.settings-path-row code { min-width:0; flex:1; overflow:hidden; padding:11px 13px; border:1px solid #dde3e0; border-radius:8px; background:#f2f5f3; color:#46514c; text-overflow:ellipsis; white-space:nowrap; font-size:12px; }.settings-apply-row { padding:16px 0 20px; border-bottom:1px solid #edf0ee; }.settings-diagnostic-header { display:flex; justify-content:space-between; padding:20px 0 12px; color:#66706b; font-size:13px; }.settings-text-button { border:0; background:transparent; color:#247a63; cursor:pointer; font:inherit; }.settings-diagnostics { display:grid; gap:8px; }.settings-diagnostic { display:flex; align-items:center; gap:12px; min-width:0; padding:13px; border:1px solid #edf0ee; border-radius:8px; }.settings-diagnostic-icon { display:grid; place-items:center; width:18px; height:18px; border:1px solid #929a96; border-radius:50%; color:#929a96; font-size:11px; }.settings-diagnostic-icon.ok { border-color:#2e8b68; color:#2e8b68; }.settings-diagnostic span { color:#3d4742; font-size:13px; }.settings-diagnostic small { margin-left:auto; overflow:hidden; color:#66706b; text-overflow:ellipsis; white-space:nowrap; }.settings-danger-card { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:18px; border-color:#f0c9c9; background:#fffafa; }.settings-danger-card strong { color:#4c3e3e; font-size:14px; }.settings-danger-card p { margin:6px 0 0; color:#7b7474; font-size:12px; }.settings-danger-divider { grid-column:1/-1; height:1px; background:#f2dddd; }.settings-danger-button { align-self:center; border:1px solid #e3a6a6; border-radius:8px; padding:9px 13px; background:#fffafa; color:#c85a5a; cursor:pointer; font:600 13px inherit; }.settings-danger-button:disabled { opacity:.55; cursor:not-allowed; }.settings-confirm-overlay { position:absolute; z-index:5; inset:0; display:grid; place-items:center; background:rgba(32,39,36,.22); }.settings-confirm { width:min(380px,calc(100% - 32px)); padding:22px; border:1px solid #dde3e0; border-radius:10px; background:#fff; box-shadow:0 12px 30px rgba(33,44,39,.16); }.settings-confirm h2 { margin:0; font-size:17px; }.settings-confirm p { color:#66706b; font-size:13px; line-height:1.6; }.settings-confirm div { display:flex; justify-content:flex-end; gap:8px; }.settings-empty-state { display:grid; flex:1; place-content:center; color:#66706b; text-align:center; }.settings-empty-state h2 { color:#202724; }
|
||||||
|
@media (max-width:900px) { .settings-sidebar { width:248px; flex-basis:248px; }.settings-page-content { padding:24px; }.settings-page-header { padding:20px 24px; }.settings-account-overview { grid-template-columns:56px 1fr; gap:16px; }.settings-account-actions { grid-column:1/-1; grid-template-columns:repeat(2,max-content); } }
|
||||||
.api-center-layout > * { min-width: 0; min-height: 0; box-sizing: border-box; }
|
.api-center-layout > * { min-width: 0; min-height: 0; box-sizing: border-box; }
|
||||||
.api-section-heading h2, .api-introduction h2, .api-integrations h2, .api-runtime-title h2 { margin: 0; color: var(--wxex-text-primary); font: 700 17px/24px var(--wxex-font); }
|
.api-section-heading h2, .api-introduction h2, .api-integrations h2, .api-runtime-title h2 { margin: 0; color: var(--wxex-text-primary); font: 700 17px/24px var(--wxex-font); }
|
||||||
.ready-text { color: var(--wxex-success); }
|
.ready-text { color: var(--wxex-success); }
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Legacy fallback: SETTINGS-01 moved the default entry to features/settings.
|
||||||
|
// Keep this panel intact until its database-key, image-key, AI and API sections are migrated.
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
|
|
||||||
interface SelfInfo {
|
interface SelfInfo {
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { SettingsEmptyState } from './components/SettingsEmptyState'
|
||||||
|
import { SettingsSidebar } from './components/SettingsSidebar'
|
||||||
|
import { SETTINGS_CATEGORY_LABELS } from './model/settingsNavigation'
|
||||||
|
import type { SettingsCategoryId, SettingsSelfInfo } from './model/types'
|
||||||
|
import { AccountDatabasePage } from './pages/AccountDatabasePage'
|
||||||
|
|
||||||
|
export function SettingsWorkspace({
|
||||||
|
selectedCategory,
|
||||||
|
onCategoryChange,
|
||||||
|
selfInfo,
|
||||||
|
dbReady,
|
||||||
|
dbKey,
|
||||||
|
onConnectionChanged,
|
||||||
|
onNotice,
|
||||||
|
onOpenSettings
|
||||||
|
}: {
|
||||||
|
selectedCategory: SettingsCategoryId
|
||||||
|
onCategoryChange: (id: SettingsCategoryId) => void
|
||||||
|
selfInfo: SettingsSelfInfo | null
|
||||||
|
dbReady: boolean
|
||||||
|
dbKey: string
|
||||||
|
onConnectionChanged: () => void
|
||||||
|
onNotice: (message: string) => void
|
||||||
|
onOpenSettings: () => void
|
||||||
|
}): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<div className="settings-workspace">
|
||||||
|
<SettingsSidebar
|
||||||
|
selectedId={selectedCategory}
|
||||||
|
onSelect={onCategoryChange}
|
||||||
|
selfInfo={selfInfo}
|
||||||
|
dbReady={dbReady}
|
||||||
|
onOpenSettings={onOpenSettings}
|
||||||
|
/>
|
||||||
|
{selectedCategory === 'account-database' ? (
|
||||||
|
<AccountDatabasePage
|
||||||
|
dbKey={dbKey}
|
||||||
|
dbReady={dbReady}
|
||||||
|
selfInfo={selfInfo}
|
||||||
|
onConnectionChanged={onConnectionChanged}
|
||||||
|
onNotice={onNotice}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<SettingsEmptyState label={SETTINGS_CATEGORY_LABELS[selectedCategory]} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
|
import { buildConnectionDiagnostics } from '../utils/buildConnectionDiagnostics'
|
||||||
|
import type { ConnectionStatus, SettingsSelfInfo } from '../model/types'
|
||||||
|
|
||||||
|
interface AppSettings {
|
||||||
|
dbRoot: string
|
||||||
|
imageAesKey: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
|
export function useAccountDatabaseController({
|
||||||
|
dbKey,
|
||||||
|
dbReady,
|
||||||
|
selfInfo,
|
||||||
|
onConnectionChanged,
|
||||||
|
onNotice
|
||||||
|
}: {
|
||||||
|
dbKey: string
|
||||||
|
dbReady: boolean
|
||||||
|
selfInfo: SettingsSelfInfo | null
|
||||||
|
onConnectionChanged: () => void
|
||||||
|
onNotice: (message: string) => void
|
||||||
|
}) {
|
||||||
|
const [settings, setSettings] = useState<AppSettings | null>(null)
|
||||||
|
const [pendingRoot, setPendingRoot] = useState('')
|
||||||
|
const [status, setStatus] = useState<ConnectionStatus>(dbReady ? 'success' : 'unavailable')
|
||||||
|
const [isTesting, setIsTesting] = useState(false)
|
||||||
|
const [lastVerifiedAt, setLastVerifiedAt] = useState<string | null>(null)
|
||||||
|
const [confirmDisconnect, setConfirmDisconnect] = useState(false)
|
||||||
|
|
||||||
|
const refresh = useCallback(async () => {
|
||||||
|
const result = await window.api.getSettings()
|
||||||
|
setSettings(result.settings)
|
||||||
|
setPendingRoot(result.settings.dbRoot)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void refresh()
|
||||||
|
}, [refresh])
|
||||||
|
useEffect(() => setStatus(dbReady ? 'success' : 'unavailable'), [dbReady])
|
||||||
|
|
||||||
|
const diagnostics = useMemo(
|
||||||
|
() =>
|
||||||
|
buildConnectionDiagnostics({
|
||||||
|
dbReady,
|
||||||
|
selfInfo,
|
||||||
|
hasDbKey: Boolean(dbKey.trim()),
|
||||||
|
hasImageKey: Boolean(settings?.imageAesKey.trim())
|
||||||
|
}),
|
||||||
|
[dbKey, dbReady, selfInfo, settings?.imageAesKey]
|
||||||
|
)
|
||||||
|
|
||||||
|
const chooseDirectory = async (): Promise<void> => {
|
||||||
|
const result = await window.api.selectDbRoot()
|
||||||
|
if (!result.canceled && result.path) setPendingRoot(result.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyDirectory = async (): Promise<void> => {
|
||||||
|
if (!pendingRoot.trim()) return onNotice('请先选择数据库目录')
|
||||||
|
setStatus('checking')
|
||||||
|
const saved = await window.api.setSettings({ dbRoot: pendingRoot.trim() })
|
||||||
|
setSettings(saved.settings)
|
||||||
|
const result = await window.api.reopenWithRoot(saved.settings.dbRoot)
|
||||||
|
if (!result.success) {
|
||||||
|
setStatus('error')
|
||||||
|
return onNotice(result.error || '重新初始化失败')
|
||||||
|
}
|
||||||
|
setStatus('success')
|
||||||
|
setLastVerifiedAt(new Date().toLocaleString('zh-CN', { hour12: false }))
|
||||||
|
onConnectionChanged()
|
||||||
|
onNotice('数据库目录已应用并重新初始化')
|
||||||
|
}
|
||||||
|
|
||||||
|
const testConnection = async (): Promise<void> => {
|
||||||
|
if (isTesting) return
|
||||||
|
setIsTesting(true)
|
||||||
|
setStatus('checking')
|
||||||
|
try {
|
||||||
|
const result = await window.api.testConnection(dbKey, pendingRoot || settings?.dbRoot)
|
||||||
|
if (!result.success) {
|
||||||
|
setStatus('error')
|
||||||
|
onNotice(result.error || '连接测试失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setStatus('success')
|
||||||
|
setLastVerifiedAt(new Date().toLocaleString('zh-CN', { hour12: false }))
|
||||||
|
onNotice('连接验证通过')
|
||||||
|
} finally {
|
||||||
|
setIsTesting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openAccountDirectory = async (): Promise<void> => {
|
||||||
|
const result = await window.api.openAccountRoot()
|
||||||
|
if (!result.success) onNotice(result.error || '打开账号目录失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
const disconnect = async (): Promise<void> => {
|
||||||
|
const result = await window.api.disconnectDb()
|
||||||
|
setConfirmDisconnect(false)
|
||||||
|
if (!result.success) return onNotice(result.error || '断开连接失败')
|
||||||
|
setStatus('unavailable')
|
||||||
|
onConnectionChanged()
|
||||||
|
onNotice('数据库连接已断开,微信原始数据未被修改')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
settings,
|
||||||
|
pendingRoot,
|
||||||
|
status,
|
||||||
|
diagnostics,
|
||||||
|
isTesting,
|
||||||
|
lastVerifiedAt,
|
||||||
|
confirmDisconnect,
|
||||||
|
setConfirmDisconnect,
|
||||||
|
chooseDirectory,
|
||||||
|
applyDirectory,
|
||||||
|
testConnection,
|
||||||
|
openAccountDirectory,
|
||||||
|
disconnect
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export function SettingsEmptyState({ label }: { label: string }): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<div className="settings-empty-state">
|
||||||
|
<h2>{label}</h2>
|
||||||
|
<p>该设置将在后续阶段接入。</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { useMemo, useState } from 'react'
|
||||||
|
import { AccountSummary } from '../../../components/account/AccountSummary'
|
||||||
|
import { SETTINGS_NAVIGATION } from '../model/settingsNavigation'
|
||||||
|
import type { SettingsCategoryId, SettingsSelfInfo } from '../model/types'
|
||||||
|
|
||||||
|
export function SettingsSidebar({
|
||||||
|
selectedId,
|
||||||
|
onSelect,
|
||||||
|
selfInfo,
|
||||||
|
dbReady,
|
||||||
|
onOpenSettings
|
||||||
|
}: {
|
||||||
|
selectedId: SettingsCategoryId
|
||||||
|
onSelect: (id: SettingsCategoryId) => void
|
||||||
|
selfInfo: SettingsSelfInfo | null
|
||||||
|
dbReady: boolean
|
||||||
|
onOpenSettings: () => void
|
||||||
|
}): React.ReactElement {
|
||||||
|
const [keyword, setKeyword] = useState('')
|
||||||
|
const groups = useMemo(
|
||||||
|
() =>
|
||||||
|
SETTINGS_NAVIGATION.map((group) => ({
|
||||||
|
...group,
|
||||||
|
items: group.items.filter((item) => item.label.includes(keyword.trim()))
|
||||||
|
})).filter((group) => group.items.length),
|
||||||
|
[keyword]
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<aside className="settings-sidebar">
|
||||||
|
<header>
|
||||||
|
<h1>设置</h1>
|
||||||
|
<p>管理账号、数据连接和本地能力</p>
|
||||||
|
<label className="settings-search">
|
||||||
|
<span>⌕</span>
|
||||||
|
<input
|
||||||
|
value={keyword}
|
||||||
|
onChange={(event) => setKeyword(event.target.value)}
|
||||||
|
placeholder="搜索设置"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</header>
|
||||||
|
<div className="settings-sidebar-list">
|
||||||
|
{groups.map((group) => (
|
||||||
|
<section key={group.label}>
|
||||||
|
<h2>{group.label}</h2>
|
||||||
|
{group.items.map((item) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
key={item.id}
|
||||||
|
className={selectedId === item.id ? 'active' : ''}
|
||||||
|
onClick={() => onSelect(item.id)}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="settings-sidebar-account">
|
||||||
|
<AccountSummary selfInfo={selfInfo} dbReady={dbReady} onClick={onOpenSettings} />
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import type { SettingsCategoryId } from './types'
|
||||||
|
|
||||||
|
export interface SettingsNavigationGroup {
|
||||||
|
label: string
|
||||||
|
items: { id: SettingsCategoryId; label: string }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SETTINGS_NAVIGATION: SettingsNavigationGroup[] = [
|
||||||
|
{
|
||||||
|
label: '连接',
|
||||||
|
items: [
|
||||||
|
{ id: 'account-database', label: '账号与数据库' },
|
||||||
|
{ id: 'database-key', label: '数据库密钥' },
|
||||||
|
{ id: 'image-key', label: '图片解密' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '智能能力',
|
||||||
|
items: [
|
||||||
|
{ id: 'ai-model', label: 'AI 模型' },
|
||||||
|
{ id: 'local-api', label: '本地 API' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '数据管理',
|
||||||
|
items: [
|
||||||
|
{ id: 'storage-export', label: '存储与导出' },
|
||||||
|
{ id: 'cache-cleanup', label: '缓存与清理' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '应用',
|
||||||
|
items: [
|
||||||
|
{ id: 'appearance', label: '外观与行为' },
|
||||||
|
{ id: 'advanced', label: '高级' },
|
||||||
|
{ id: 'about', label: '关于' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export const SETTINGS_CATEGORY_LABELS = Object.fromEntries(
|
||||||
|
SETTINGS_NAVIGATION.flatMap((group) => group.items.map((item) => [item.id, item.label]))
|
||||||
|
) as Record<SettingsCategoryId, string>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
export type SettingsCategoryId =
|
||||||
|
| 'account-database'
|
||||||
|
| 'database-key'
|
||||||
|
| 'image-key'
|
||||||
|
| 'ai-model'
|
||||||
|
| 'local-api'
|
||||||
|
| 'storage-export'
|
||||||
|
| 'cache-cleanup'
|
||||||
|
| 'appearance'
|
||||||
|
| 'advanced'
|
||||||
|
| 'about'
|
||||||
|
|
||||||
|
export type ConnectionStatus = 'idle' | 'checking' | 'success' | 'warning' | 'error' | 'unavailable'
|
||||||
|
|
||||||
|
export interface SettingsSelfInfo {
|
||||||
|
wxid: string
|
||||||
|
nickname: string
|
||||||
|
avatar?: string
|
||||||
|
accountRoot: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiagnosticItem {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
status: ConnectionStatus
|
||||||
|
result: string
|
||||||
|
detail?: string
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
import { useAccountDatabaseController } from '../account-database/useAccountDatabaseController'
|
||||||
|
import type { SettingsSelfInfo } from '../model/types'
|
||||||
|
|
||||||
|
function ShieldIcon(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M12 3.5 19 6v5.8c0 4.5-3 7.5-7 8.7-4-1.2-7-4.2-7-8.7V6l7-2.5Z" />
|
||||||
|
<path d="m9 12 2 2 4-4" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
function StatusIcon({ ok }: { ok: boolean }): React.ReactElement {
|
||||||
|
return <span className={`settings-diagnostic-icon ${ok ? 'ok' : ''}`}>{ok ? '✓' : '—'}</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AccountDatabasePage({
|
||||||
|
dbKey,
|
||||||
|
dbReady,
|
||||||
|
selfInfo,
|
||||||
|
onConnectionChanged,
|
||||||
|
onNotice
|
||||||
|
}: {
|
||||||
|
dbKey: string
|
||||||
|
dbReady: boolean
|
||||||
|
selfInfo: SettingsSelfInfo | null
|
||||||
|
onConnectionChanged: () => void
|
||||||
|
onNotice: (message: string) => void
|
||||||
|
}): React.ReactElement {
|
||||||
|
const controller = useAccountDatabaseController({
|
||||||
|
dbKey,
|
||||||
|
dbReady,
|
||||||
|
selfInfo,
|
||||||
|
onConnectionChanged,
|
||||||
|
onNotice
|
||||||
|
})
|
||||||
|
const connected = controller.status === 'success'
|
||||||
|
return (
|
||||||
|
<div className="settings-page">
|
||||||
|
<header className="settings-page-header">
|
||||||
|
<div>
|
||||||
|
<h1>账号与数据库</h1>
|
||||||
|
<p>管理当前微信账号以及本地数据库连接</p>
|
||||||
|
</div>
|
||||||
|
<span className={`settings-status-badge ${controller.status}`}>
|
||||||
|
{controller.status === 'checking'
|
||||||
|
? '正在验证'
|
||||||
|
: connected
|
||||||
|
? '连接正常'
|
||||||
|
: controller.status === 'error'
|
||||||
|
? '连接异常'
|
||||||
|
: '尚未连接'}
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
<div className="settings-page-scroll">
|
||||||
|
<div className="settings-page-content">
|
||||||
|
<section className="settings-privacy-notice">
|
||||||
|
<ShieldIcon />
|
||||||
|
<div>
|
||||||
|
<strong>数据仅在本机读取</strong>
|
||||||
|
<p>WechatExplorer 不会将您的微信聊天数据上传到云端。所有解析和存储均在本地完成。</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<h2 className="settings-section-heading">账号概览</h2>
|
||||||
|
<section className="settings-card settings-account-overview">
|
||||||
|
<div className="settings-avatar">
|
||||||
|
{selfInfo?.avatar ? (
|
||||||
|
<img src={selfInfo.avatar} alt="当前账号" referrerPolicy="no-referrer" />
|
||||||
|
) : (
|
||||||
|
(selfInfo?.nickname || '?').charAt(0)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="settings-account-meta">
|
||||||
|
<span>昵称</span>
|
||||||
|
<strong>{selfInfo?.nickname || '暂无数据'}</strong>
|
||||||
|
<span>最近验证</span>
|
||||||
|
<strong>{controller.lastVerifiedAt || '暂无数据'}</strong>
|
||||||
|
</div>
|
||||||
|
<div className="settings-account-meta">
|
||||||
|
<span>WXID</span>
|
||||||
|
<strong>{selfInfo?.wxid || '暂无数据'}</strong>
|
||||||
|
<span>数据库连接状态</span>
|
||||||
|
<strong className={connected ? 'success-text' : ''}>
|
||||||
|
{connected ? '连接正常' : '尚未连接'}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
<div className="settings-account-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="api-primary-button"
|
||||||
|
onClick={() => void controller.testConnection()}
|
||||||
|
disabled={controller.isTesting}
|
||||||
|
>
|
||||||
|
{controller.isTesting ? '验证中...' : '重新验证'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="api-secondary-button"
|
||||||
|
onClick={() => void controller.openAccountDirectory()}
|
||||||
|
>
|
||||||
|
打开账号目录
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<h2 className="settings-section-heading">数据库连接</h2>
|
||||||
|
<section className="settings-card">
|
||||||
|
<label className="settings-field-label">数据库根目录</label>
|
||||||
|
<div className="settings-path-row">
|
||||||
|
<code title={controller.pendingRoot}>{controller.pendingRoot || '暂无数据'}</code>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="api-secondary-button"
|
||||||
|
onClick={() => void controller.chooseDirectory()}
|
||||||
|
>
|
||||||
|
更改目录
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="settings-apply-row">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="api-primary-button"
|
||||||
|
onClick={() => void controller.applyDirectory()}
|
||||||
|
disabled={!controller.pendingRoot || controller.isTesting}
|
||||||
|
>
|
||||||
|
应用并重新初始化
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="settings-diagnostic-header">
|
||||||
|
<span>连接状态自检</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="settings-text-button"
|
||||||
|
onClick={() => void controller.testConnection()}
|
||||||
|
disabled={controller.isTesting}
|
||||||
|
>
|
||||||
|
{controller.isTesting ? '正在测试...' : '重新测试连接'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="settings-diagnostics">
|
||||||
|
{controller.diagnostics.map((item) => (
|
||||||
|
<div className="settings-diagnostic" key={item.id}>
|
||||||
|
<StatusIcon ok={item.status === 'success'} />
|
||||||
|
<span>{item.label}</span>
|
||||||
|
<small title={item.detail}>{item.result}</small>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<h2 className="settings-section-heading danger">连接管理</h2>
|
||||||
|
<section className="settings-card settings-danger-card">
|
||||||
|
<div>
|
||||||
|
<strong>断开数据库连接</strong>
|
||||||
|
<p>断开后 WechatExplorer 暂时无法读取聊天记录,但不会删除微信原始数据。</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="settings-danger-button"
|
||||||
|
onClick={() => controller.setConfirmDisconnect(true)}
|
||||||
|
disabled={!dbReady}
|
||||||
|
>
|
||||||
|
断开连接
|
||||||
|
</button>
|
||||||
|
<div className="settings-danger-divider" />
|
||||||
|
<div>
|
||||||
|
<strong>重置连接配置</strong>
|
||||||
|
<p>重置逻辑尚未提供完整的安全边界,本阶段暂不开放。</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" className="settings-danger-button" disabled>
|
||||||
|
暂未开放
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{controller.confirmDisconnect && (
|
||||||
|
<div className="settings-confirm-overlay" role="dialog" aria-modal="true">
|
||||||
|
<div className="settings-confirm">
|
||||||
|
<h2>断开数据库连接?</h2>
|
||||||
|
<p>这不会删除微信原始数据。你可以稍后重新连接。</p>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="api-secondary-button"
|
||||||
|
onClick={() => controller.setConfirmDisconnect(false)}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="settings-danger-button"
|
||||||
|
onClick={() => void controller.disconnect()}
|
||||||
|
>
|
||||||
|
确认断开
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import type { DiagnosticItem, SettingsSelfInfo } from '../model/types'
|
||||||
|
|
||||||
|
export function buildConnectionDiagnostics(input: {
|
||||||
|
dbReady: boolean
|
||||||
|
selfInfo: SettingsSelfInfo | null
|
||||||
|
hasDbKey: boolean
|
||||||
|
hasImageKey: boolean
|
||||||
|
}): DiagnosticItem[] {
|
||||||
|
const connected = input.dbReady
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'db-key',
|
||||||
|
label: '数据库密钥校验',
|
||||||
|
status: input.hasDbKey ? (connected ? 'success' : 'warning') : 'unavailable',
|
||||||
|
result: input.hasDbKey ? (connected ? '已用于当前连接' : '已保存,未验证') : '未检测'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'contacts',
|
||||||
|
label: '联系人索引',
|
||||||
|
status: connected ? 'success' : 'unavailable',
|
||||||
|
result: connected ? '当前连接可读取' : '未检测'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'messages',
|
||||||
|
label: '消息数据库挂载',
|
||||||
|
status: connected ? 'success' : 'unavailable',
|
||||||
|
result: connected ? '当前连接已挂载' : '未检测'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'identity',
|
||||||
|
label: '账号身份识别',
|
||||||
|
status: input.selfInfo?.wxid ? 'success' : 'unavailable',
|
||||||
|
result: input.selfInfo?.wxid ? '匹配成功' : '未检测'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'image-key',
|
||||||
|
label: '图片解密密钥',
|
||||||
|
status: input.hasImageKey ? 'success' : 'unavailable',
|
||||||
|
result: input.hasImageKey ? '已配置' : '未检测',
|
||||||
|
detail: input.hasImageKey ? undefined : '图片密钥将在后续页面管理'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user