mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-18 03:57:02 +08:00
fix: 优化数据库启动与设置页响应性能
(cherry picked from commit f6614548b0e742bc703b2a5aaa5060720985ed27)
This commit is contained in:
+5
-1
@@ -486,6 +486,7 @@ app.whenReady().then(async () => {
|
|||||||
if (dbInitInFlight) return dbInitInFlight
|
if (dbInitInFlight) return dbInitInFlight
|
||||||
|
|
||||||
dbInitInFlight = (async () => {
|
dbInitInFlight = (async () => {
|
||||||
|
const startedAt = Date.now()
|
||||||
try {
|
try {
|
||||||
if (wcdbBootstrapPromise) await wcdbBootstrapPromise
|
if (wcdbBootstrapPromise) await wcdbBootstrapPromise
|
||||||
const trimmedKey = String(key || '').trim()
|
const trimmedKey = String(key || '').trim()
|
||||||
@@ -511,7 +512,7 @@ app.whenReady().then(async () => {
|
|||||||
}
|
}
|
||||||
chat.setChatDb(nextWechatDb)
|
chat.setChatDb(nextWechatDb)
|
||||||
const wcdb4Client = nextWechatDb.getWcdb4Client()
|
const wcdb4Client = nextWechatDb.getWcdb4Client()
|
||||||
const sessions = await wcdb4Client.getSessionsAsync()
|
const sessions = await wcdb4Client.getSessionsAsync({ hydrateDisplayNames: false })
|
||||||
configureRecallProtection(wcdb4Client, resolvedRoot, settings.recallProtectionEnabled)
|
configureRecallProtection(wcdb4Client, resolvedRoot, settings.recallProtectionEnabled)
|
||||||
voiceService = new VoiceService(wcdb4Client)
|
voiceService = new VoiceService(wcdb4Client)
|
||||||
stickerService = new StickerService(wcdb4Client)
|
stickerService = new StickerService(wcdb4Client)
|
||||||
@@ -530,6 +531,9 @@ app.whenReady().then(async () => {
|
|||||||
.catch((error) => console.warn('[WCDB4] message cursor warmup failed:', error))
|
.catch((error) => console.warn('[WCDB4] message cursor warmup failed:', error))
|
||||||
}
|
}
|
||||||
imageDecryptService = null
|
imageDecryptService = null
|
||||||
|
console.log(
|
||||||
|
`[WCDB4] db:init ready sessions=${sessions.length} monitoring=${monitoring} cost=${Date.now() - startedAt}ms`
|
||||||
|
)
|
||||||
return { success: true, monitoring }
|
return { success: true, monitoring }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to init DB:', error)
|
console.error('Failed to init DB:', error)
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ export function listContacts(filter?: string): FormattedContact[] {
|
|||||||
|
|
||||||
export async function listContactsAsync(filter?: string): Promise<FormattedContact[]> {
|
export async function listContactsAsync(filter?: string): Promise<FormattedContact[]> {
|
||||||
if (!dbRef) return []
|
if (!dbRef) return []
|
||||||
await dbRef.getWcdb4Client().getSessionsAsync()
|
await dbRef.getWcdb4Client().getSessionsAsync({ hydrateDisplayNames: false })
|
||||||
return listContacts(filter)
|
return listContacts(filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+53
-16
@@ -32,6 +32,10 @@ export interface Wcdb4MessageQueryOptions {
|
|||||||
limit?: number
|
limit?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Wcdb4SessionQueryOptions {
|
||||||
|
hydrateDisplayNames?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
type Wcdb4MessageStore = {
|
type Wcdb4MessageStore = {
|
||||||
tableName: string
|
tableName: string
|
||||||
dbPath: string
|
dbPath: string
|
||||||
@@ -242,6 +246,8 @@ export class Wcdb4Client {
|
|||||||
private cachedSessions: Wcdb4Session[] | null = null
|
private cachedSessions: Wcdb4Session[] | null = null
|
||||||
private cachedChatTables: { name: string; db_number: string }[] | null = null
|
private cachedChatTables: { name: string; db_number: string }[] | null = null
|
||||||
private sessionsInFlight: Promise<Wcdb4Session[]> | null = null
|
private sessionsInFlight: Promise<Wcdb4Session[]> | null = null
|
||||||
|
private sessionDisplayNamesInFlight: Promise<void> | null = null
|
||||||
|
private sessionDisplayNamesHydrated = false
|
||||||
private sessionCacheGeneration = 0
|
private sessionCacheGeneration = 0
|
||||||
|
|
||||||
private wcdbShutdown: (() => number) | null = null
|
private wcdbShutdown: (() => number) | null = null
|
||||||
@@ -558,6 +564,7 @@ export class Wcdb4Client {
|
|||||||
|
|
||||||
this.handle = null
|
this.handle = null
|
||||||
this.cachedSessions = null
|
this.cachedSessions = null
|
||||||
|
this.sessionDisplayNamesHydrated = false
|
||||||
this.displayNameCache.clear()
|
this.displayNameCache.clear()
|
||||||
this.avatarCache.clear()
|
this.avatarCache.clear()
|
||||||
this.groupNicknameCache.clear()
|
this.groupNicknameCache.clear()
|
||||||
@@ -722,13 +729,22 @@ export class Wcdb4Client {
|
|||||||
...session,
|
...session,
|
||||||
nickname: this.displayNameCache.get(session.username) || session.nickname || session.username
|
nickname: this.displayNameCache.get(session.username) || session.nickname || session.username
|
||||||
}))
|
}))
|
||||||
|
this.sessionDisplayNamesHydrated = true
|
||||||
|
|
||||||
return this.cachedSessions
|
return this.cachedSessions
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSessionsAsync(): Promise<Wcdb4Session[]> {
|
async getSessionsAsync(options: Wcdb4SessionQueryOptions = {}): Promise<Wcdb4Session[]> {
|
||||||
if (this.cachedSessions) return this.cachedSessions
|
const hydrateDisplayNames = options.hydrateDisplayNames !== false
|
||||||
if (this.sessionsInFlight) return this.sessionsInFlight
|
if (this.cachedSessions) {
|
||||||
|
if (hydrateDisplayNames) await this.ensureSessionDisplayNamesAsync()
|
||||||
|
return this.cachedSessions
|
||||||
|
}
|
||||||
|
if (this.sessionsInFlight) {
|
||||||
|
await this.sessionsInFlight
|
||||||
|
if (hydrateDisplayNames) await this.ensureSessionDisplayNamesAsync()
|
||||||
|
return this.cachedSessions || []
|
||||||
|
}
|
||||||
if (!this.wcdbGetSessions) return []
|
if (!this.wcdbGetSessions) return []
|
||||||
|
|
||||||
const generation = this.sessionCacheGeneration
|
const generation = this.sessionCacheGeneration
|
||||||
@@ -739,25 +755,17 @@ export class Wcdb4Client {
|
|||||||
const sessions = (Array.isArray(rows) ? rows : [])
|
const sessions = (Array.isArray(rows) ? rows : [])
|
||||||
.map((row) => this.normalizeSession(row))
|
.map((row) => this.normalizeSession(row))
|
||||||
.filter((session) => session.username)
|
.filter((session) => session.username)
|
||||||
await this.hydrateDisplayNamesAsync(
|
if (generation === this.sessionCacheGeneration) this.cachedSessions = sessions
|
||||||
sessions
|
return sessions
|
||||||
.filter((session) => this.shouldHydrateSessionDisplayName(session))
|
|
||||||
.map((session) => session.username)
|
|
||||||
)
|
|
||||||
const hydrated = sessions.map((session) => ({
|
|
||||||
...session,
|
|
||||||
nickname:
|
|
||||||
this.displayNameCache.get(session.username) || session.nickname || session.username
|
|
||||||
}))
|
|
||||||
if (generation === this.sessionCacheGeneration) this.cachedSessions = hydrated
|
|
||||||
return hydrated
|
|
||||||
})()
|
})()
|
||||||
this.sessionsInFlight = request
|
this.sessionsInFlight = request
|
||||||
try {
|
try {
|
||||||
return await request
|
await request
|
||||||
} finally {
|
} finally {
|
||||||
if (this.sessionsInFlight === request) this.sessionsInFlight = null
|
if (this.sessionsInFlight === request) this.sessionsInFlight = null
|
||||||
}
|
}
|
||||||
|
if (hydrateDisplayNames) await this.ensureSessionDisplayNamesAsync()
|
||||||
|
return this.cachedSessions || []
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidateSessionCache(): void {
|
invalidateSessionCache(): void {
|
||||||
@@ -765,6 +773,7 @@ export class Wcdb4Client {
|
|||||||
this.cachedSessions = null
|
this.cachedSessions = null
|
||||||
this.cachedChatTables = null
|
this.cachedChatTables = null
|
||||||
this.sessionsInFlight = null
|
this.sessionsInFlight = null
|
||||||
|
this.sessionDisplayNamesHydrated = false
|
||||||
}
|
}
|
||||||
|
|
||||||
getChatTables(): { name: string; db_number: string }[] {
|
getChatTables(): { name: string; db_number: string }[] {
|
||||||
@@ -2214,6 +2223,34 @@ export class Wcdb4Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async ensureSessionDisplayNamesAsync(): Promise<void> {
|
||||||
|
if (this.sessionDisplayNamesHydrated || !this.cachedSessions) return
|
||||||
|
if (this.sessionDisplayNamesInFlight) return this.sessionDisplayNamesInFlight
|
||||||
|
|
||||||
|
const generation = this.sessionCacheGeneration
|
||||||
|
const sessions = this.cachedSessions
|
||||||
|
const request = (async (): Promise<void> => {
|
||||||
|
await this.hydrateDisplayNamesAsync(
|
||||||
|
sessions
|
||||||
|
.filter((session) => this.shouldHydrateSessionDisplayName(session))
|
||||||
|
.map((session) => session.username)
|
||||||
|
)
|
||||||
|
if (generation !== this.sessionCacheGeneration || this.cachedSessions !== sessions) return
|
||||||
|
this.cachedSessions = sessions.map((session) => ({
|
||||||
|
...session,
|
||||||
|
nickname:
|
||||||
|
this.displayNameCache.get(session.username) || session.nickname || session.username
|
||||||
|
}))
|
||||||
|
this.sessionDisplayNamesHydrated = true
|
||||||
|
})()
|
||||||
|
this.sessionDisplayNamesInFlight = request
|
||||||
|
try {
|
||||||
|
await request
|
||||||
|
} finally {
|
||||||
|
if (this.sessionDisplayNamesInFlight === request) this.sessionDisplayNamesInFlight = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private hydrateAvatarUrls(usernames: string[]): void {
|
private hydrateAvatarUrls(usernames: string[]): void {
|
||||||
const missing = this.uniq(usernames).filter((username) => !this.avatarCache.has(username))
|
const missing = this.uniq(usernames).filter((username) => !this.avatarCache.has(username))
|
||||||
if (missing.length === 0) return
|
if (missing.length === 0) return
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ const mergeMessagePages = (older: Message[], current: 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 [isDatabaseConnected, setIsDatabaseConnected] = useState(false)
|
||||||
|
const [isDatabaseConnecting, setIsDatabaseConnecting] = useState(false)
|
||||||
const [dbKey, setDbKey] = useState(getDevelopmentDatabaseKey)
|
const [dbKey, setDbKey] = useState(getDevelopmentDatabaseKey)
|
||||||
const [contacts, setContacts] = useState<Contact[]>([])
|
const [contacts, setContacts] = useState<Contact[]>([])
|
||||||
const [selectedContact, setSelectedContact] = useState<Contact | null>(null)
|
const [selectedContact, setSelectedContact] = useState<Contact | null>(null)
|
||||||
@@ -582,6 +583,7 @@ function App(): React.ReactElement {
|
|||||||
if (!autoLoginEnabled) return
|
if (!autoLoginEnabled) return
|
||||||
try {
|
try {
|
||||||
const startupCacheReady = await loadStartupCache()
|
const startupCacheReady = await loadStartupCache()
|
||||||
|
setIsDatabaseConnecting(true)
|
||||||
const initPromise = window.api.initDb(key)
|
const initPromise = window.api.initDb(key)
|
||||||
if (startupCacheReady) {
|
if (startupCacheReady) {
|
||||||
setIsAuthenticated(true)
|
setIsAuthenticated(true)
|
||||||
@@ -606,6 +608,9 @@ function App(): React.ReactElement {
|
|||||||
console.warn('[Startup] background database init failed:', error)
|
console.warn('[Startup] background database init failed:', error)
|
||||||
setDbKeyStatusKind('error')
|
setDbKeyStatusKind('error')
|
||||||
})
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (active) setIsDatabaseConnecting(false)
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const result = await initPromise
|
const result = await initPromise
|
||||||
@@ -638,6 +643,8 @@ function App(): React.ReactElement {
|
|||||||
setDbKeyStatus(`自动连接失败: ${message}`)
|
setDbKeyStatus(`自动连接失败: ${message}`)
|
||||||
setDbKeyStatusKind('error')
|
setDbKeyStatusKind('error')
|
||||||
setBootState('login')
|
setBootState('login')
|
||||||
|
} finally {
|
||||||
|
if (active) setIsDatabaseConnecting(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void attemptAutoConnect()
|
void attemptAutoConnect()
|
||||||
@@ -663,6 +670,7 @@ function App(): React.ReactElement {
|
|||||||
const keyToUse = keyInput || dbKey
|
const keyToUse = keyInput || dbKey
|
||||||
if (!keyToUse) return
|
if (!keyToUse) return
|
||||||
setBootState('connecting')
|
setBootState('connecting')
|
||||||
|
setIsDatabaseConnecting(true)
|
||||||
// 持久化用户手动指定的微信聊天文件路径,供 db:init 读取 settings.dbRoot
|
// 持久化用户手动指定的微信聊天文件路径,供 db:init 读取 settings.dbRoot
|
||||||
const trimmedRoot = dbRootInput.trim()
|
const trimmedRoot = dbRootInput.trim()
|
||||||
if (trimmedRoot) {
|
if (trimmedRoot) {
|
||||||
@@ -746,6 +754,8 @@ function App(): React.ReactElement {
|
|||||||
setBootState('login')
|
setBootState('login')
|
||||||
setStartupProgress(null)
|
setStartupProgress(null)
|
||||||
alert('Error connecting to database')
|
alert('Error connecting to database')
|
||||||
|
} finally {
|
||||||
|
setIsDatabaseConnecting(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -918,6 +928,7 @@ function App(): React.ReactElement {
|
|||||||
const handleReturnToLogin = (): void => {
|
const handleReturnToLogin = (): void => {
|
||||||
setIsAuthenticated(false)
|
setIsAuthenticated(false)
|
||||||
setIsDatabaseConnected(false)
|
setIsDatabaseConnected(false)
|
||||||
|
setIsDatabaseConnecting(false)
|
||||||
setBootState('login')
|
setBootState('login')
|
||||||
setDatabaseConnectionMode(dbKey ? 'manual' : 'automatic')
|
setDatabaseConnectionMode(dbKey ? 'manual' : 'automatic')
|
||||||
setActivePage('archive')
|
setActivePage('archive')
|
||||||
@@ -1425,6 +1436,7 @@ function App(): React.ReactElement {
|
|||||||
width={sidebarWidth}
|
width={sidebarWidth}
|
||||||
selfInfo={selfInfo}
|
selfInfo={selfInfo}
|
||||||
dbReady={isDatabaseConnected}
|
dbReady={isDatabaseConnected}
|
||||||
|
dbConnecting={isDatabaseConnecting}
|
||||||
onOpenSettings={openSettings}
|
onOpenSettings={openSettings}
|
||||||
/>
|
/>
|
||||||
<div className="resizer" onMouseDown={startResizing} />
|
<div className="resizer" onMouseDown={startResizing} />
|
||||||
@@ -1454,6 +1466,7 @@ function App(): React.ReactElement {
|
|||||||
selectedReportId={selectedReportId}
|
selectedReportId={selectedReportId}
|
||||||
selfInfo={selfInfo}
|
selfInfo={selfInfo}
|
||||||
dbReady={isDatabaseConnected}
|
dbReady={isDatabaseConnected}
|
||||||
|
dbConnecting={isDatabaseConnecting}
|
||||||
onSelectReport={openReport}
|
onSelectReport={openReport}
|
||||||
onCreateReport={openReportConfigure}
|
onCreateReport={openReportConfigure}
|
||||||
onDeleteReport={handleDeleteReport}
|
onDeleteReport={handleDeleteReport}
|
||||||
@@ -1476,6 +1489,7 @@ function App(): React.ReactElement {
|
|||||||
selectedContact={reportSourceContact}
|
selectedContact={reportSourceContact}
|
||||||
selfInfo={selfInfo}
|
selfInfo={selfInfo}
|
||||||
dbReady={isDatabaseConnected}
|
dbReady={isDatabaseConnected}
|
||||||
|
dbConnecting={isDatabaseConnecting}
|
||||||
onSelectContact={handleSelectReportSource}
|
onSelectContact={handleSelectReportSource}
|
||||||
onOpenSettings={openSettings}
|
onOpenSettings={openSettings}
|
||||||
/>
|
/>
|
||||||
@@ -1546,6 +1560,7 @@ function App(): React.ReactElement {
|
|||||||
onCategoryChange={setSettingsCategory}
|
onCategoryChange={setSettingsCategory}
|
||||||
selfInfo={selfInfo}
|
selfInfo={selfInfo}
|
||||||
dbReady={isDatabaseConnected}
|
dbReady={isDatabaseConnected}
|
||||||
|
dbConnecting={isDatabaseConnecting}
|
||||||
dbKey={dbKey}
|
dbKey={dbKey}
|
||||||
onDbKeyChange={setDbKey}
|
onDbKeyChange={setDbKey}
|
||||||
onDatabaseConnectionChange={setIsDatabaseConnected}
|
onDatabaseConnectionChange={setIsDatabaseConnected}
|
||||||
@@ -1692,6 +1707,7 @@ function App(): React.ReactElement {
|
|||||||
activePage={activePage}
|
activePage={activePage}
|
||||||
selfInfo={selfInfo}
|
selfInfo={selfInfo}
|
||||||
dbReady={isDatabaseConnected}
|
dbReady={isDatabaseConnected}
|
||||||
|
dbConnecting={isDatabaseConnecting}
|
||||||
onPageChange={handlePageChange}
|
onPageChange={handlePageChange}
|
||||||
onOpenSettings={openSettings}
|
onOpenSettings={openSettings}
|
||||||
onOpenGuide={openFirstUseGuide}
|
onOpenGuide={openFirstUseGuide}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ interface SelfInfo {
|
|||||||
interface AccountSummaryProps {
|
interface AccountSummaryProps {
|
||||||
selfInfo: SelfInfo | null
|
selfInfo: SelfInfo | null
|
||||||
dbReady: boolean
|
dbReady: boolean
|
||||||
|
dbConnecting?: boolean
|
||||||
compact?: boolean
|
compact?: boolean
|
||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
}
|
}
|
||||||
@@ -17,13 +18,20 @@ interface AccountSummaryProps {
|
|||||||
export function AccountSummary({
|
export function AccountSummary({
|
||||||
selfInfo,
|
selfInfo,
|
||||||
dbReady,
|
dbReady,
|
||||||
|
dbConnecting = false,
|
||||||
compact = false,
|
compact = false,
|
||||||
onClick
|
onClick
|
||||||
}: AccountSummaryProps): React.ReactElement {
|
}: AccountSummaryProps): React.ReactElement {
|
||||||
|
const showAccount = Boolean(selfInfo && (dbReady || dbConnecting))
|
||||||
const displayName =
|
const displayName =
|
||||||
dbReady && selfInfo ? selfInfo.nickname || selfInfo.wxid || '当前账号' : '未连接'
|
showAccount && selfInfo ? selfInfo.nickname || selfInfo.wxid || '当前账号' : '未连接'
|
||||||
const subtitle = dbReady && selfInfo ? selfInfo.wxid : '打开设置'
|
const subtitle = showAccount && selfInfo ? selfInfo.wxid : '打开设置'
|
||||||
const statusText = dbReady ? '数据库已连接' : '数据库未连接'
|
const statusText = dbReady
|
||||||
|
? '数据库已连接'
|
||||||
|
: dbConnecting
|
||||||
|
? '正在连接数据库'
|
||||||
|
: '数据库未连接'
|
||||||
|
const statusClass = dbReady ? 'ready' : dbConnecting ? 'connecting' : ''
|
||||||
const initial = (displayName || '?').charAt(0)
|
const initial = (displayName || '?').charAt(0)
|
||||||
const title = `${displayName}\n${subtitle}`
|
const title = `${displayName}\n${subtitle}`
|
||||||
const avatar = (
|
const avatar = (
|
||||||
@@ -33,7 +41,7 @@ export function AccountSummary({
|
|||||||
) : (
|
) : (
|
||||||
initial
|
initial
|
||||||
)}
|
)}
|
||||||
<span className={`account-summary-status ${dbReady ? 'ready' : ''}`} aria-hidden />
|
<span className={`account-summary-status ${statusClass}`} aria-hidden />
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -52,7 +60,7 @@ export function AccountSummary({
|
|||||||
<span className="account-summary-name">{displayName}</span>
|
<span className="account-summary-name">{displayName}</span>
|
||||||
<span className="account-summary-meta">{subtitle}</span>
|
<span className="account-summary-meta">{subtitle}</span>
|
||||||
<span className="account-summary-state">
|
<span className="account-summary-state">
|
||||||
<span className={`account-summary-state-dot ${dbReady ? 'ready' : ''}`} aria-hidden />
|
<span className={`account-summary-state-dot ${statusClass}`} aria-hidden />
|
||||||
{statusText}
|
{statusText}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export interface ConversationSidebarProps {
|
|||||||
width: number
|
width: number
|
||||||
selfInfo: SelfInfo | null
|
selfInfo: SelfInfo | null
|
||||||
dbReady: boolean
|
dbReady: boolean
|
||||||
|
dbConnecting?: boolean
|
||||||
onOpenSettings: () => void
|
onOpenSettings: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ export function ConversationSidebar({
|
|||||||
width,
|
width,
|
||||||
selfInfo,
|
selfInfo,
|
||||||
dbReady,
|
dbReady,
|
||||||
|
dbConnecting = false,
|
||||||
onOpenSettings
|
onOpenSettings
|
||||||
}: ConversationSidebarProps): React.ReactElement {
|
}: ConversationSidebarProps): React.ReactElement {
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
@@ -124,7 +126,12 @@ export function ConversationSidebar({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="conversation-sidebar-account">
|
<div className="conversation-sidebar-account">
|
||||||
<AccountSummary selfInfo={selfInfo} dbReady={dbReady} onClick={onOpenSettings} />
|
<AccountSummary
|
||||||
|
selfInfo={selfInfo}
|
||||||
|
dbReady={dbReady}
|
||||||
|
dbConnecting={dbConnecting}
|
||||||
|
onClick={onOpenSettings}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ interface AppShellProps {
|
|||||||
activePage: AppPage
|
activePage: AppPage
|
||||||
selfInfo: SelfInfo | null
|
selfInfo: SelfInfo | null
|
||||||
dbReady: boolean
|
dbReady: boolean
|
||||||
|
dbConnecting?: boolean
|
||||||
onPageChange: (page: AppPage) => void
|
onPageChange: (page: AppPage) => void
|
||||||
onOpenSettings: () => void
|
onOpenSettings: () => void
|
||||||
onOpenGuide: () => void
|
onOpenGuide: () => void
|
||||||
@@ -35,6 +36,7 @@ export function AppShell({
|
|||||||
activePage,
|
activePage,
|
||||||
selfInfo,
|
selfInfo,
|
||||||
dbReady,
|
dbReady,
|
||||||
|
dbConnecting = false,
|
||||||
onPageChange,
|
onPageChange,
|
||||||
onOpenSettings,
|
onOpenSettings,
|
||||||
onOpenGuide,
|
onOpenGuide,
|
||||||
@@ -63,7 +65,13 @@ export function AppShell({
|
|||||||
<span className="app-guide-launcher-label">新手引导</span>
|
<span className="app-guide-launcher-label">新手引导</span>
|
||||||
</button>
|
</button>
|
||||||
<div className="app-rail-account">
|
<div className="app-rail-account">
|
||||||
<AccountSummary selfInfo={selfInfo} dbReady={dbReady} compact onClick={onOpenSettings} />
|
<AccountSummary
|
||||||
|
selfInfo={selfInfo}
|
||||||
|
dbReady={dbReady}
|
||||||
|
dbConnecting={dbConnecting}
|
||||||
|
compact
|
||||||
|
onClick={onOpenSettings}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
<main className="app-shell-main" aria-label={activeItem?.label || '工作区'}>
|
<main className="app-shell-main" aria-label={activeItem?.label || '工作区'}>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ interface ReportHistorySidebarProps {
|
|||||||
selectedReportId: string | null
|
selectedReportId: string | null
|
||||||
selfInfo: SelfInfo | null
|
selfInfo: SelfInfo | null
|
||||||
dbReady: boolean
|
dbReady: boolean
|
||||||
|
dbConnecting?: boolean
|
||||||
onSelectReport: (reportId: string) => void
|
onSelectReport: (reportId: string) => void
|
||||||
onCreateReport: () => void
|
onCreateReport: () => void
|
||||||
onDeleteReport: (reportId: string) => Promise<{ success: boolean; error?: string }>
|
onDeleteReport: (reportId: string) => Promise<{ success: boolean; error?: string }>
|
||||||
@@ -80,6 +81,7 @@ export function ReportHistorySidebar({
|
|||||||
selectedReportId,
|
selectedReportId,
|
||||||
selfInfo,
|
selfInfo,
|
||||||
dbReady,
|
dbReady,
|
||||||
|
dbConnecting = false,
|
||||||
onSelectReport,
|
onSelectReport,
|
||||||
onCreateReport,
|
onCreateReport,
|
||||||
onDeleteReport,
|
onDeleteReport,
|
||||||
@@ -202,7 +204,12 @@ export function ReportHistorySidebar({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="report-history-account">
|
<div className="report-history-account">
|
||||||
<AccountSummary selfInfo={selfInfo} dbReady={dbReady} onClick={onOpenSettings} />
|
<AccountSummary
|
||||||
|
selfInfo={selfInfo}
|
||||||
|
dbReady={dbReady}
|
||||||
|
dbConnecting={dbConnecting}
|
||||||
|
onClick={onOpenSettings}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{pendingDelete && (
|
{pendingDelete && (
|
||||||
<div className="report-delete-confirm" role="dialog" aria-modal="true">
|
<div className="report-delete-confirm" role="dialog" aria-modal="true">
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ interface ReportSourceSidebarProps {
|
|||||||
selectedContact: Contact | null
|
selectedContact: Contact | null
|
||||||
selfInfo: SelfInfo | null
|
selfInfo: SelfInfo | null
|
||||||
dbReady: boolean
|
dbReady: boolean
|
||||||
|
dbConnecting?: boolean
|
||||||
onSelectContact: (contact: Contact) => void
|
onSelectContact: (contact: Contact) => void
|
||||||
onOpenSettings: () => void
|
onOpenSettings: () => void
|
||||||
}
|
}
|
||||||
@@ -23,6 +24,7 @@ export function ReportSourceSidebar({
|
|||||||
selectedContact,
|
selectedContact,
|
||||||
selfInfo,
|
selfInfo,
|
||||||
dbReady,
|
dbReady,
|
||||||
|
dbConnecting = false,
|
||||||
onSelectContact,
|
onSelectContact,
|
||||||
onOpenSettings
|
onOpenSettings
|
||||||
}: ReportSourceSidebarProps): React.ReactElement {
|
}: ReportSourceSidebarProps): React.ReactElement {
|
||||||
@@ -95,6 +97,7 @@ export function ReportSourceSidebar({
|
|||||||
<AccountSummary
|
<AccountSummary
|
||||||
selfInfo={selfInfo}
|
selfInfo={selfInfo}
|
||||||
dbReady={dbReady}
|
dbReady={dbReady}
|
||||||
|
dbConnecting={dbConnecting}
|
||||||
onClick={onOpenSettings}
|
onClick={onOpenSettings}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export function SettingsWorkspace({
|
|||||||
onCategoryChange,
|
onCategoryChange,
|
||||||
selfInfo,
|
selfInfo,
|
||||||
dbReady,
|
dbReady,
|
||||||
|
dbConnecting = false,
|
||||||
dbKey,
|
dbKey,
|
||||||
onDbKeyChange,
|
onDbKeyChange,
|
||||||
onDatabaseConnectionChange,
|
onDatabaseConnectionChange,
|
||||||
@@ -35,6 +36,7 @@ export function SettingsWorkspace({
|
|||||||
onCategoryChange: (id: SettingsCategoryId) => void
|
onCategoryChange: (id: SettingsCategoryId) => void
|
||||||
selfInfo: SettingsSelfInfo | null
|
selfInfo: SettingsSelfInfo | null
|
||||||
dbReady: boolean
|
dbReady: boolean
|
||||||
|
dbConnecting?: boolean
|
||||||
dbKey: string
|
dbKey: string
|
||||||
onDbKeyChange: (key: string) => void
|
onDbKeyChange: (key: string) => void
|
||||||
onDatabaseConnectionChange: (connected: boolean) => void
|
onDatabaseConnectionChange: (connected: boolean) => void
|
||||||
@@ -47,26 +49,20 @@ export function SettingsWorkspace({
|
|||||||
onOpenSettings: () => void
|
onOpenSettings: () => void
|
||||||
onAppearanceChange: (settings: { theme: 'system' | 'light' | 'dark'; compactMode: boolean }) => void
|
onAppearanceChange: (settings: { theme: 'system' | 'light' | 'dark'; compactMode: boolean }) => void
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
|
const renderSelectedPage = (): React.ReactElement => {
|
||||||
|
switch (selectedCategory) {
|
||||||
|
case 'account-database':
|
||||||
return (
|
return (
|
||||||
<div className="settings-workspace">
|
|
||||||
<SettingsSidebar
|
|
||||||
selectedId={selectedCategory}
|
|
||||||
onSelect={onCategoryChange}
|
|
||||||
selfInfo={selfInfo}
|
|
||||||
dbReady={dbReady}
|
|
||||||
onOpenSettings={onOpenSettings}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className={`settings-page-panel ${selectedCategory === 'account-database' ? 'active' : ''}`}
|
|
||||||
>
|
|
||||||
<AccountDatabasePage
|
<AccountDatabasePage
|
||||||
dbKey={dbKey}
|
dbKey={dbKey}
|
||||||
dbReady={dbReady}
|
dbReady={dbReady}
|
||||||
|
dbConnecting={dbConnecting}
|
||||||
selfInfo={selfInfo}
|
selfInfo={selfInfo}
|
||||||
onNotice={onNotice}
|
onNotice={onNotice}
|
||||||
/>
|
/>
|
||||||
</div>
|
)
|
||||||
<div className={`settings-page-panel ${selectedCategory === 'database-key' ? 'active' : ''}`}>
|
case 'database-key':
|
||||||
|
return (
|
||||||
<DatabaseKeyPage
|
<DatabaseKeyPage
|
||||||
dbKey={dbKey}
|
dbKey={dbKey}
|
||||||
dbReady={dbReady}
|
dbReady={dbReady}
|
||||||
@@ -79,45 +75,39 @@ export function SettingsWorkspace({
|
|||||||
onReturnToLogin={onReturnToLogin}
|
onReturnToLogin={onReturnToLogin}
|
||||||
onNotice={onNotice}
|
onNotice={onNotice}
|
||||||
/>
|
/>
|
||||||
</div>
|
)
|
||||||
<div className={`settings-page-panel ${selectedCategory === 'image-key' ? 'active' : ''}`}>
|
case 'image-key':
|
||||||
<ImageDecryptionPage selfInfo={selfInfo} onNotice={onNotice} />
|
return <ImageDecryptionPage selfInfo={selfInfo} onNotice={onNotice} />
|
||||||
</div>
|
case 'ai-model':
|
||||||
<div className={`settings-page-panel ${selectedCategory === 'ai-model' ? 'active' : ''}`}>
|
return <AIModelPage onRuntimeChange={onAIRuntimeChange} onNotice={onNotice} />
|
||||||
<AIModelPage onRuntimeChange={onAIRuntimeChange} onNotice={onNotice} />
|
case 'recall-protection':
|
||||||
</div>
|
return <RecallProtectionPage onNotice={onNotice} />
|
||||||
<div
|
case 'advanced':
|
||||||
className={`settings-page-panel ${selectedCategory === 'recall-protection' ? 'active' : ''}`}
|
return <AdvancedPage onNotice={onNotice} />
|
||||||
>
|
case 'cache-cleanup':
|
||||||
<RecallProtectionPage onNotice={onNotice} />
|
return <CacheCleanupPage onNotice={onNotice} />
|
||||||
</div>
|
case 'appearance':
|
||||||
<div className={`settings-page-panel ${selectedCategory === 'advanced' ? 'active' : ''}`}>
|
return (
|
||||||
<AdvancedPage onNotice={onNotice} />
|
|
||||||
</div>
|
|
||||||
<div className={`settings-page-panel ${selectedCategory === 'cache-cleanup' ? 'active' : ''}`}>
|
|
||||||
<CacheCleanupPage onNotice={onNotice} />
|
|
||||||
</div>
|
|
||||||
<div className={`settings-page-panel ${selectedCategory === 'appearance' ? 'active' : ''}`}>
|
|
||||||
<AppearancePage onNotice={onNotice} onAppearanceChange={onAppearanceChange} />
|
<AppearancePage onNotice={onNotice} onAppearanceChange={onAppearanceChange} />
|
||||||
</div>
|
)
|
||||||
<div className={`settings-page-panel ${selectedCategory === 'about' ? 'active' : ''}`}>
|
case 'about':
|
||||||
<AboutPage onNotice={onNotice} />
|
return <AboutPage onNotice={onNotice} />
|
||||||
</div>
|
default:
|
||||||
{![
|
return <SettingsEmptyState label={SETTINGS_CATEGORY_LABELS[selectedCategory]} />
|
||||||
'account-database',
|
}
|
||||||
'database-key',
|
}
|
||||||
'image-key',
|
|
||||||
'ai-model',
|
return (
|
||||||
'recall-protection',
|
<div className="settings-workspace">
|
||||||
'advanced',
|
<SettingsSidebar
|
||||||
'cache-cleanup',
|
selectedId={selectedCategory}
|
||||||
'appearance',
|
onSelect={onCategoryChange}
|
||||||
'about'
|
selfInfo={selfInfo}
|
||||||
].includes(selectedCategory) && (
|
dbReady={dbReady}
|
||||||
<div className="settings-page-panel active">
|
dbConnecting={dbConnecting}
|
||||||
<SettingsEmptyState label={SETTINGS_CATEGORY_LABELS[selectedCategory]} />
|
onOpenSettings={onOpenSettings}
|
||||||
</div>
|
/>
|
||||||
)}
|
<div className="settings-page-panel active">{renderSelectedPage()}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ import type { ConnectionCheckState } from './types'
|
|||||||
export function useAccountDatabaseController({
|
export function useAccountDatabaseController({
|
||||||
dbKey,
|
dbKey,
|
||||||
dbReady,
|
dbReady,
|
||||||
|
dbConnecting = false,
|
||||||
selfInfo,
|
selfInfo,
|
||||||
onNotice
|
onNotice
|
||||||
}: {
|
}: {
|
||||||
dbKey: string
|
dbKey: string
|
||||||
dbReady: boolean
|
dbReady: boolean
|
||||||
|
dbConnecting?: boolean
|
||||||
selfInfo: SettingsSelfInfo | null
|
selfInfo: SettingsSelfInfo | null
|
||||||
onNotice: (message: string) => void
|
onNotice: (message: string) => void
|
||||||
}) {
|
}) {
|
||||||
@@ -28,7 +30,7 @@ export function useAccountDatabaseController({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let active = true
|
let active = true
|
||||||
void window.api
|
void window.api
|
||||||
.getImageDecryptionStatus()
|
.getImageKeyConfig()
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (active) setHasImageKey(result.configured)
|
if (active) setHasImageKey(result.configured)
|
||||||
})
|
})
|
||||||
@@ -58,8 +60,11 @@ export function useAccountDatabaseController({
|
|||||||
[checkState, dbKey, dbReady, hasImageKey, selfInfo]
|
[checkState, dbKey, dbReady, hasImageKey, selfInfo]
|
||||||
)
|
)
|
||||||
const connectionStatus = useMemo(
|
const connectionStatus = useMemo(
|
||||||
() => getConnectionOverviewStatus({ dbReady, checkState, diagnostics }),
|
() =>
|
||||||
[checkState, dbReady, diagnostics]
|
dbConnecting
|
||||||
|
? ('checking' as const)
|
||||||
|
: getConnectionOverviewStatus({ dbReady, checkState, diagnostics }),
|
||||||
|
[checkState, dbConnecting, dbReady, diagnostics]
|
||||||
)
|
)
|
||||||
const lastCheckedLabel = formatConnectionCheckedAt(checkState, clock)
|
const lastCheckedLabel = formatConnectionCheckedAt(checkState, clock)
|
||||||
|
|
||||||
@@ -121,7 +126,7 @@ export function useAccountDatabaseController({
|
|||||||
diagnostics,
|
diagnostics,
|
||||||
connectionStatus,
|
connectionStatus,
|
||||||
checkState,
|
checkState,
|
||||||
isChecking: checkState.status === 'checking',
|
isChecking: dbConnecting || checkState.status === 'checking',
|
||||||
lastCheckedLabel,
|
lastCheckedLabel,
|
||||||
testConnection,
|
testConnection,
|
||||||
openAccountDirectory,
|
openAccountDirectory,
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ export function SettingsSidebar({
|
|||||||
onSelect,
|
onSelect,
|
||||||
selfInfo,
|
selfInfo,
|
||||||
dbReady,
|
dbReady,
|
||||||
|
dbConnecting = false,
|
||||||
onOpenSettings
|
onOpenSettings
|
||||||
}: {
|
}: {
|
||||||
selectedId: SettingsCategoryId
|
selectedId: SettingsCategoryId
|
||||||
onSelect: (id: SettingsCategoryId) => void
|
onSelect: (id: SettingsCategoryId) => void
|
||||||
selfInfo: SettingsSelfInfo | null
|
selfInfo: SettingsSelfInfo | null
|
||||||
dbReady: boolean
|
dbReady: boolean
|
||||||
|
dbConnecting?: boolean
|
||||||
onOpenSettings: () => void
|
onOpenSettings: () => void
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const [keyword, setKeyword] = useState('')
|
const [keyword, setKeyword] = useState('')
|
||||||
@@ -57,7 +59,12 @@ export function SettingsSidebar({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-sidebar-account">
|
<div className="settings-sidebar-account">
|
||||||
<AccountSummary selfInfo={selfInfo} dbReady={dbReady} onClick={onOpenSettings} />
|
<AccountSummary
|
||||||
|
selfInfo={selfInfo}
|
||||||
|
dbReady={dbReady}
|
||||||
|
dbConnecting={dbConnecting}
|
||||||
|
onClick={onOpenSettings}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,15 +17,23 @@ const STATUS_LABELS: Record<ConnectionOverviewStatus, string> = {
|
|||||||
export function AccountDatabasePage({
|
export function AccountDatabasePage({
|
||||||
dbKey,
|
dbKey,
|
||||||
dbReady,
|
dbReady,
|
||||||
|
dbConnecting = false,
|
||||||
selfInfo,
|
selfInfo,
|
||||||
onNotice
|
onNotice
|
||||||
}: {
|
}: {
|
||||||
dbKey: string
|
dbKey: string
|
||||||
dbReady: boolean
|
dbReady: boolean
|
||||||
|
dbConnecting?: boolean
|
||||||
selfInfo: SettingsSelfInfo | null
|
selfInfo: SettingsSelfInfo | null
|
||||||
onNotice: (message: string) => void
|
onNotice: (message: string) => void
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const controller = useAccountDatabaseController({ dbKey, dbReady, selfInfo, onNotice })
|
const controller = useAccountDatabaseController({
|
||||||
|
dbKey,
|
||||||
|
dbReady,
|
||||||
|
dbConnecting,
|
||||||
|
selfInfo,
|
||||||
|
onNotice
|
||||||
|
})
|
||||||
const [autoLogin, setAutoLogin] = useState(false)
|
const [autoLogin, setAutoLogin] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
+8
@@ -253,6 +253,10 @@ body {
|
|||||||
&.ready {
|
&.ready {
|
||||||
background: var(--wxex-success);
|
background: var(--wxex-success);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.connecting {
|
||||||
|
background: var(--wxex-brand);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-summary:not(.compact) .account-summary-avatar .account-summary-status {
|
.account-summary:not(.compact) .account-summary-avatar .account-summary-status {
|
||||||
@@ -306,6 +310,10 @@ body {
|
|||||||
&.ready {
|
&.ready {
|
||||||
background: var(--wxex-success);
|
background: var(--wxex-success);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.connecting {
|
||||||
|
background: var(--wxex-brand);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-summary-settings {
|
.account-summary-settings {
|
||||||
|
|||||||
Reference in New Issue
Block a user