mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-17 19:47:08 +08:00
精简账号与数据库设置页
This commit is contained in:
@@ -810,12 +810,6 @@ function App(): React.ReactElement {
|
||||
setActivePage('settings')
|
||||
}
|
||||
|
||||
const handleSettingsConnectionChanged = (): void => {
|
||||
void refreshSelfInfo()
|
||||
void loadContacts()
|
||||
void window.api.getSelf().then((result) => setIsDatabaseConnected(result.ready))
|
||||
}
|
||||
|
||||
const openReport = (reportId: string): void => {
|
||||
setSelectedReportId(reportId)
|
||||
setReportWorkspaceView('result')
|
||||
@@ -1112,7 +1106,6 @@ function App(): React.ReactElement {
|
||||
selfInfo={selfInfo}
|
||||
dbReady={isDatabaseConnected}
|
||||
dbKey={dbKey}
|
||||
onConnectionChanged={handleSettingsConnectionChanged}
|
||||
onNotice={(message) => {
|
||||
setReportNotice(message)
|
||||
window.setTimeout(() => setReportNotice(''), 3200)
|
||||
|
||||
@@ -3554,9 +3554,75 @@ body {
|
||||
.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); } }
|
||||
.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; gap:18px; 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; flex:0 0 auto; 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.checking { background:#eef4f8; color:#4f7188; }
|
||||
.settings-status-badge.warning { background:#fff6e8; color:#a56a24; }
|
||||
.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 { box-sizing:border-box; width:min(100%, 860px); 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-card { border:1px solid #dde3e0; border-radius:10px; background:#fff; padding:24px 26px; }
|
||||
.settings-account-overview { display:grid; grid-template-columns:minmax(190px,1fr) minmax(220px,1fr) auto; gap:24px; align-items:center; }
|
||||
.settings-account-primary { display:flex; min-width:0; align-items:center; gap:15px; }
|
||||
.settings-avatar { width:66px; height:66px; flex:0 0 auto; 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-identity { display:grid; min-width:0; gap:3px; }
|
||||
.settings-account-identity span,.settings-account-root>span,.settings-account-facts dt { color:#929a96; font-size:12px; }
|
||||
.settings-account-identity strong,.settings-account-identity small { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.settings-account-identity strong { color:#35403b; font-size:14px; }
|
||||
.settings-account-identity small { color:#66706b; font:12px/18px ui-monospace,SFMono-Regular,Consolas,monospace; }
|
||||
.settings-account-facts { display:grid; min-width:0; gap:12px; margin:0; }
|
||||
.settings-account-facts div { min-width:0; }
|
||||
.settings-account-facts dd { overflow:hidden; margin:3px 0 0; color:#35403b; font-size:13px; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.settings-connection-text.success { color:#2e8b68; }
|
||||
.settings-connection-text.warning { color:#a56a24; }
|
||||
.settings-connection-text.error { color:#c85a5a; }
|
||||
.settings-account-actions { display:grid; gap:8px; }
|
||||
.settings-account-actions button { min-height:34px; white-space:nowrap; }
|
||||
.settings-account-root { grid-column:1/-1; display:grid; min-width:0; gap:7px; padding-top:19px; border-top:1px solid #edf0ee; }
|
||||
.settings-account-root>div { display:flex; min-width:0; align-items:center; gap:8px; }
|
||||
.settings-account-root code { min-width:0; flex:1; overflow:hidden; padding:10px 12px; border:1px solid #dde3e0; border-radius:8px; background:#f2f5f3; color:#46514c; text-overflow:ellipsis; white-space:nowrap; font:12px/18px ui-monospace,SFMono-Regular,Consolas,monospace; }
|
||||
.settings-icon-button { display:grid; width:36px; height:36px; flex:0 0 auto; place-items:center; border:1px solid #dde3e0; border-radius:8px; background:#fff; color:#66706b; cursor:pointer; }
|
||||
.settings-icon-button:hover:not(:disabled) { border-color:#247a63; color:#247a63; }
|
||||
.settings-icon-button:disabled { cursor:not-allowed; opacity:.45; }
|
||||
.settings-icon-button svg { width:17px; fill:none; stroke:currentColor; stroke-linecap:round; stroke-linejoin:round; stroke-width:1.8; }
|
||||
.settings-health-card { padding:12px; }
|
||||
.settings-diagnostic-summary { margin:0 0 10px; padding:9px 11px; border-left:3px solid #c68635; border-radius:6px; background:#fff8ec; color:#79501f; font-size:12px; line-height:1.55; }
|
||||
.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; width:18px; height:18px; flex:0 0 auto; place-items:center; border:1px solid #929a96; border-radius:50%; color:#929a96; font-size:11px; font-weight:700; }
|
||||
.settings-diagnostic-icon.success { border-color:#2e8b68; color:#2e8b68; }
|
||||
.settings-diagnostic-icon.warning { border-color:#c68635; color:#c68635; }
|
||||
.settings-diagnostic-icon.error { border-color:#c85a5a; color:#c85a5a; }
|
||||
.settings-diagnostic-icon.checking { border-color:#cbd7d2; border-top-color:#247a63; animation:settings-spin .8s linear infinite; }
|
||||
.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-empty-state { display:grid; flex:1; place-content:center; color:#66706b; text-align:center; }
|
||||
.settings-empty-state h2 { color:#202724; }
|
||||
@keyframes settings-spin { to { transform:rotate(360deg); } }
|
||||
@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:minmax(0,1fr) auto; gap:18px; }
|
||||
.settings-account-facts { grid-column:1/-1; grid-template-columns:repeat(2,minmax(0,1fr)); }
|
||||
.settings-account-actions { grid-row:1; grid-column:2; }
|
||||
}
|
||||
@media (max-width:700px) {
|
||||
.settings-account-overview { grid-template-columns:minmax(0,1fr); }
|
||||
.settings-account-actions { grid-row:auto; grid-column:1; grid-template-columns:repeat(2,minmax(0,1fr)); }
|
||||
.settings-account-root { grid-column:1; }
|
||||
}
|
||||
.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); }
|
||||
.ready-text { color: var(--wxex-success); }
|
||||
|
||||
@@ -10,7 +10,6 @@ export function SettingsWorkspace({
|
||||
selfInfo,
|
||||
dbReady,
|
||||
dbKey,
|
||||
onConnectionChanged,
|
||||
onNotice,
|
||||
onOpenSettings
|
||||
}: {
|
||||
@@ -19,7 +18,6 @@ export function SettingsWorkspace({
|
||||
selfInfo: SettingsSelfInfo | null
|
||||
dbReady: boolean
|
||||
dbKey: string
|
||||
onConnectionChanged: () => void
|
||||
onNotice: (message: string) => void
|
||||
onOpenSettings: () => void
|
||||
}): React.ReactElement {
|
||||
@@ -37,7 +35,6 @@ export function SettingsWorkspace({
|
||||
dbKey={dbKey}
|
||||
dbReady={dbReady}
|
||||
selfInfo={selfInfo}
|
||||
onConnectionChanged={onConnectionChanged}
|
||||
onNotice={onNotice}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { SettingsSelfInfo } from '../model/types'
|
||||
import type { ConnectionOverviewStatus } from './types'
|
||||
|
||||
function CopyIcon(): React.ReactElement {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<rect x="8" y="8" width="11" height="11" rx="2" />
|
||||
<path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const CONNECTION_LABELS: Record<ConnectionOverviewStatus, string> = {
|
||||
checking: '正在检测',
|
||||
success: '连接正常',
|
||||
warning: '部分能力不可用',
|
||||
error: '连接异常',
|
||||
unavailable: '尚未连接'
|
||||
}
|
||||
|
||||
export function AccountOverview({
|
||||
selfInfo,
|
||||
connectionStatus,
|
||||
lastCheckedLabel,
|
||||
isChecking,
|
||||
onCheck,
|
||||
onOpenDirectory,
|
||||
onCopyDirectory
|
||||
}: {
|
||||
selfInfo: SettingsSelfInfo | null
|
||||
connectionStatus: ConnectionOverviewStatus
|
||||
lastCheckedLabel: string
|
||||
isChecking: boolean
|
||||
onCheck: () => void
|
||||
onOpenDirectory: () => void
|
||||
onCopyDirectory: () => void
|
||||
}): React.ReactElement {
|
||||
const accountRoot = selfInfo?.accountRoot || ''
|
||||
return (
|
||||
<section className="settings-card settings-account-overview">
|
||||
<div className="settings-account-primary">
|
||||
<div className="settings-avatar">
|
||||
{selfInfo?.avatar ? (
|
||||
<img src={selfInfo.avatar} alt="当前账号" referrerPolicy="no-referrer" />
|
||||
) : (
|
||||
(selfInfo?.nickname || '?').charAt(0)
|
||||
)}
|
||||
</div>
|
||||
<div className="settings-account-identity">
|
||||
<span>昵称</span>
|
||||
<strong>{selfInfo?.nickname || '暂无数据'}</strong>
|
||||
<small title={selfInfo?.wxid || undefined}>{selfInfo?.wxid || '暂无 WXID'}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dl className="settings-account-facts">
|
||||
<div>
|
||||
<dt>数据库连接状态</dt>
|
||||
<dd className={`settings-connection-text ${connectionStatus}`}>
|
||||
{CONNECTION_LABELS[connectionStatus]}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>最近检测时间</dt>
|
||||
<dd>{lastCheckedLabel}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div className="settings-account-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="api-primary-button"
|
||||
onClick={onCheck}
|
||||
disabled={isChecking}
|
||||
>
|
||||
{isChecking ? '正在检测...' : '重新检测'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="api-secondary-button"
|
||||
onClick={onOpenDirectory}
|
||||
disabled={!accountRoot}
|
||||
>
|
||||
打开账号目录
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="settings-account-root">
|
||||
<span>当前账号目录</span>
|
||||
<div>
|
||||
<code title={accountRoot || undefined}>{accountRoot || '暂无数据'}</code>
|
||||
<button
|
||||
type="button"
|
||||
className="settings-icon-button"
|
||||
title="复制账号目录"
|
||||
aria-label="复制账号目录"
|
||||
onClick={onCopyDirectory}
|
||||
disabled={!accountRoot}
|
||||
>
|
||||
<CopyIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { ConnectionDiagnostic, DiagnosticStatus } from './types'
|
||||
|
||||
function DiagnosticIcon({ status }: { status: DiagnosticStatus }): React.ReactElement {
|
||||
if (status === 'checking') {
|
||||
return <span className="settings-diagnostic-icon checking" aria-label="检测中" />
|
||||
}
|
||||
const symbol =
|
||||
status === 'success' ? '✓' : status === 'warning' ? '!' : status === 'error' ? '×' : '—'
|
||||
return (
|
||||
<span className={`settings-diagnostic-icon ${status}`} aria-hidden="true">
|
||||
{symbol}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export function ConnectionHealthSection({
|
||||
diagnostics,
|
||||
summary
|
||||
}: {
|
||||
diagnostics: ConnectionDiagnostic[]
|
||||
summary?: string
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<section className="settings-card settings-health-card">
|
||||
{summary && <p className="settings-diagnostic-summary">{summary}</p>}
|
||||
<div className="settings-diagnostics">
|
||||
{diagnostics.map((item) => (
|
||||
<div className="settings-diagnostic" key={item.id}>
|
||||
<DiagnosticIcon status={item.status} />
|
||||
<span>{item.label}</span>
|
||||
<small title={item.detail}>{item.result}</small>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
export function LocalPrivacyNotice(): React.ReactElement {
|
||||
return (
|
||||
<section className="settings-privacy-notice">
|
||||
<ShieldIcon />
|
||||
<div>
|
||||
<strong>数据仅在本机读取</strong>
|
||||
<p>WechatExplorer 不会将您的微信聊天数据上传到云端。所有解析和存储均在本地完成。</p>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import type { SettingsSelfInfo } from '../model/types'
|
||||
import type { ConnectionCheckState, ConnectionDiagnostic, ConnectionOverviewStatus } from './types'
|
||||
|
||||
const CORE_DIAGNOSTIC_IDS = new Set<ConnectionDiagnostic['id']>([
|
||||
'db-key',
|
||||
'contacts',
|
||||
'messages',
|
||||
'identity'
|
||||
])
|
||||
|
||||
export function buildConnectionDiagnostics(input: {
|
||||
dbReady: boolean
|
||||
selfInfo: SettingsSelfInfo | null
|
||||
hasDbKey: boolean
|
||||
hasImageKey: boolean
|
||||
checkState: ConnectionCheckState
|
||||
}): ConnectionDiagnostic[] {
|
||||
if (input.checkState.status === 'checking') {
|
||||
return [
|
||||
{ id: 'db-key', label: '数据库密钥', status: 'checking', result: '检测中' },
|
||||
{ id: 'contacts', label: '联系人索引', status: 'checking', result: '检测中' },
|
||||
{ id: 'messages', label: '消息数据库', status: 'checking', result: '检测中' },
|
||||
{ id: 'identity', label: '账号身份', status: 'checking', result: '检测中' },
|
||||
{ id: 'image-key', label: '图片解密密钥', status: 'checking', result: '检测中' }
|
||||
]
|
||||
}
|
||||
|
||||
const checkFailed = input.checkState.status === 'error'
|
||||
const checkError = input.checkState.status === 'error' ? input.checkState.message : undefined
|
||||
const connected = input.dbReady && !checkFailed
|
||||
const identityMatched =
|
||||
input.checkState.status === 'success' || input.checkState.status === 'warning'
|
||||
? input.checkState.identityMatched
|
||||
: undefined
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'db-key',
|
||||
label: '数据库密钥',
|
||||
status: checkFailed
|
||||
? 'error'
|
||||
: connected
|
||||
? 'success'
|
||||
: input.hasDbKey
|
||||
? 'warning'
|
||||
: 'unavailable',
|
||||
result: checkFailed
|
||||
? '验证失败'
|
||||
: connected
|
||||
? '已用于当前连接'
|
||||
: input.hasDbKey
|
||||
? '尚未验证'
|
||||
: '尚未配置',
|
||||
detail: checkError
|
||||
},
|
||||
{
|
||||
id: 'contacts',
|
||||
label: '联系人索引',
|
||||
status: checkFailed ? 'error' : connected ? 'success' : 'idle',
|
||||
result: checkFailed ? '读取失败' : connected ? '当前连接可读取' : '未检测'
|
||||
},
|
||||
{
|
||||
id: 'messages',
|
||||
label: '消息数据库',
|
||||
status: checkFailed ? 'error' : connected ? 'success' : 'idle',
|
||||
result: checkFailed ? '挂载失败' : connected ? '当前连接已挂载' : '未检测'
|
||||
},
|
||||
{
|
||||
id: 'identity',
|
||||
label: '账号身份',
|
||||
status:
|
||||
identityMatched === false
|
||||
? 'error'
|
||||
: connected && input.selfInfo?.wxid
|
||||
? 'success'
|
||||
: 'idle',
|
||||
result:
|
||||
identityMatched === false
|
||||
? '账号不匹配'
|
||||
: connected && input.selfInfo?.wxid
|
||||
? '匹配成功'
|
||||
: '未检测'
|
||||
},
|
||||
{
|
||||
id: 'image-key',
|
||||
label: '图片解密密钥',
|
||||
status: input.hasImageKey ? 'success' : 'warning',
|
||||
result: input.hasImageKey ? '已配置' : '未配置',
|
||||
detail: input.hasImageKey ? undefined : '不影响聊天数据库读取,可在图片解密设置中配置'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export function getConnectionOverviewStatus(input: {
|
||||
dbReady: boolean
|
||||
checkState: ConnectionCheckState
|
||||
diagnostics: ConnectionDiagnostic[]
|
||||
}): ConnectionOverviewStatus {
|
||||
if (input.checkState.status === 'checking') return 'checking'
|
||||
if (!input.dbReady) return 'unavailable'
|
||||
if (input.checkState.status === 'error') return 'error'
|
||||
|
||||
const coreDiagnostics = input.diagnostics.filter((item) => CORE_DIAGNOSTIC_IDS.has(item.id))
|
||||
if (coreDiagnostics.some((item) => item.status === 'error')) return 'error'
|
||||
if (coreDiagnostics.some((item) => item.status !== 'success')) return 'warning'
|
||||
if (
|
||||
input.diagnostics.some((item) => item.status === 'warning' || item.status === 'unavailable')
|
||||
) {
|
||||
return 'warning'
|
||||
}
|
||||
return 'success'
|
||||
}
|
||||
|
||||
export function formatConnectionCheckedAt(
|
||||
checkState: ConnectionCheckState,
|
||||
now: Date = new Date()
|
||||
): string {
|
||||
if (checkState.status === 'checking') return '正在检测'
|
||||
if (!checkState.checkedAt) return '本次启动期间尚未检测'
|
||||
|
||||
const elapsedMinutes = Math.max(
|
||||
0,
|
||||
Math.floor((now.getTime() - checkState.checkedAt.getTime()) / 60_000)
|
||||
)
|
||||
if (elapsedMinutes < 1) return '刚刚'
|
||||
if (elapsedMinutes < 60) return `${elapsedMinutes} 分钟前`
|
||||
return checkState.checkedAt.toLocaleString('zh-CN', { hour12: false })
|
||||
}
|
||||
|
||||
export function isSameAccount(input: {
|
||||
currentWxid?: string
|
||||
probeWxid?: string
|
||||
currentRoot?: string
|
||||
probeRoot?: string
|
||||
}): boolean | undefined {
|
||||
const currentWxid = input.currentWxid?.trim()
|
||||
const probeWxid = input.probeWxid?.trim()
|
||||
if (currentWxid && probeWxid) return currentWxid === probeWxid
|
||||
|
||||
const normalizePath = (value?: string): string | undefined =>
|
||||
value?.trim().replace(/\\/g, '/').replace(/\/+$/, '').toLocaleLowerCase()
|
||||
const currentRoot = normalizePath(input.currentRoot)
|
||||
const probeRoot = normalizePath(input.probeRoot)
|
||||
return currentRoot && probeRoot ? currentRoot === probeRoot : undefined
|
||||
}
|
||||
|
||||
export function sanitizeConnectionError(error?: string): string {
|
||||
const normalized = error?.toLocaleLowerCase() || ''
|
||||
if (normalized.includes('key') || normalized.includes('密钥')) return '数据库密钥验证失败'
|
||||
if (normalized.includes('path') || normalized.includes('目录')) return '无法读取当前账号目录'
|
||||
if (normalized.includes('permission') || normalized.includes('access'))
|
||||
return '当前账号目录不可访问'
|
||||
return '数据库连接检查未通过'
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export type DiagnosticStatus = 'idle' | 'checking' | 'success' | 'warning' | 'error' | 'unavailable'
|
||||
|
||||
export type ConnectionCheckState =
|
||||
| { status: 'idle'; checkedAt?: Date }
|
||||
| { status: 'checking'; checkedAt?: Date }
|
||||
| { status: 'success'; checkedAt: Date; identityMatched?: boolean }
|
||||
| { status: 'warning'; checkedAt: Date; message?: string; identityMatched?: boolean }
|
||||
| { status: 'error'; checkedAt: Date; message?: string }
|
||||
|
||||
export interface ConnectionDiagnostic {
|
||||
id: 'db-key' | 'contacts' | 'messages' | 'identity' | 'image-key'
|
||||
label: string
|
||||
status: DiagnosticStatus
|
||||
result: string
|
||||
detail?: string
|
||||
}
|
||||
|
||||
export type ConnectionOverviewStatus = 'checking' | 'success' | 'warning' | 'error' | 'unavailable'
|
||||
+87
-74
@@ -1,9 +1,15 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { buildConnectionDiagnostics } from '../utils/buildConnectionDiagnostics'
|
||||
import type { ConnectionStatus, SettingsSelfInfo } from '../model/types'
|
||||
import type { SettingsSelfInfo } from '../model/types'
|
||||
import {
|
||||
buildConnectionDiagnostics,
|
||||
formatConnectionCheckedAt,
|
||||
getConnectionOverviewStatus,
|
||||
isSameAccount,
|
||||
sanitizeConnectionError
|
||||
} from './diagnostics'
|
||||
import type { ConnectionCheckState } from './types'
|
||||
|
||||
interface AppSettings {
|
||||
dbRoot: string
|
||||
imageAesKey: string
|
||||
}
|
||||
|
||||
@@ -12,111 +18,118 @@ 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 [checkState, setCheckState] = useState<ConnectionCheckState>({ status: 'idle' })
|
||||
const [clock, setClock] = useState(() => new Date())
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
const result = await window.api.getSettings()
|
||||
setSettings(result.settings)
|
||||
setPendingRoot(result.settings.dbRoot)
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
void window.api
|
||||
.getSettings()
|
||||
.then((result) => {
|
||||
if (active) setSettings(result.settings)
|
||||
})
|
||||
.catch(() => {
|
||||
if (active) setSettings(null)
|
||||
})
|
||||
return () => {
|
||||
active = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
void refresh()
|
||||
}, [refresh])
|
||||
useEffect(() => setStatus(dbReady ? 'success' : 'unavailable'), [dbReady])
|
||||
if (!checkState.checkedAt) return
|
||||
const timer = window.setInterval(() => setClock(new Date()), 30_000)
|
||||
return () => window.clearInterval(timer)
|
||||
}, [checkState.checkedAt])
|
||||
|
||||
const hasImageKey = Boolean(settings?.imageAesKey.trim())
|
||||
const diagnostics = useMemo(
|
||||
() =>
|
||||
buildConnectionDiagnostics({
|
||||
dbReady,
|
||||
selfInfo,
|
||||
hasDbKey: Boolean(dbKey.trim()),
|
||||
hasImageKey: Boolean(settings?.imageAesKey.trim())
|
||||
hasImageKey,
|
||||
checkState
|
||||
}),
|
||||
[dbKey, dbReady, selfInfo, settings?.imageAesKey]
|
||||
[checkState, dbKey, dbReady, hasImageKey, selfInfo]
|
||||
)
|
||||
const connectionStatus = useMemo(
|
||||
() => getConnectionOverviewStatus({ dbReady, checkState, diagnostics }),
|
||||
[checkState, dbReady, diagnostics]
|
||||
)
|
||||
const lastCheckedLabel = formatConnectionCheckedAt(checkState, clock)
|
||||
|
||||
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')
|
||||
const testConnection = useCallback(async (): Promise<void> => {
|
||||
if (checkState.status === 'checking') return
|
||||
setCheckState({ status: 'checking', checkedAt: checkState.checkedAt })
|
||||
const checkedAt = new Date()
|
||||
setClock(checkedAt)
|
||||
try {
|
||||
const result = await window.api.testConnection(dbKey, pendingRoot || settings?.dbRoot)
|
||||
const result = await window.api.testConnection(dbKey, selfInfo?.accountRoot)
|
||||
if (!result.success) {
|
||||
setStatus('error')
|
||||
onNotice(result.error || '连接测试失败')
|
||||
setCheckState({
|
||||
status: 'error',
|
||||
checkedAt,
|
||||
message: sanitizeConnectionError(result.error)
|
||||
})
|
||||
onNotice('连接检测失败,请查看诊断结果')
|
||||
return
|
||||
}
|
||||
setStatus('success')
|
||||
setLastVerifiedAt(new Date().toLocaleString('zh-CN', { hour12: false }))
|
||||
onNotice('连接验证通过')
|
||||
} finally {
|
||||
setIsTesting(false)
|
||||
}
|
||||
|
||||
const identityMatched = isSameAccount({
|
||||
currentWxid: selfInfo?.wxid,
|
||||
probeWxid: result.wxid,
|
||||
currentRoot: selfInfo?.accountRoot,
|
||||
probeRoot: result.accountRoot
|
||||
})
|
||||
if (identityMatched === false) {
|
||||
setCheckState({
|
||||
status: 'warning',
|
||||
checkedAt,
|
||||
message: '当前连接账号与页面账号不一致',
|
||||
identityMatched
|
||||
})
|
||||
onNotice('连接检测失败,请查看诊断结果')
|
||||
return
|
||||
}
|
||||
|
||||
const openAccountDirectory = async (): Promise<void> => {
|
||||
setCheckState({ status: 'success', checkedAt, identityMatched })
|
||||
onNotice(hasImageKey ? '连接检测完成' : '连接可用,但部分能力需要配置')
|
||||
} catch {
|
||||
setCheckState({ status: 'error', checkedAt, message: '数据库连接检查未通过' })
|
||||
onNotice('连接检测失败,请查看诊断结果')
|
||||
}
|
||||
}, [checkState, dbKey, hasImageKey, onNotice, selfInfo])
|
||||
|
||||
const openAccountDirectory = useCallback(async (): Promise<void> => {
|
||||
const result = await window.api.openAccountRoot()
|
||||
if (!result.success) onNotice(result.error || '打开账号目录失败')
|
||||
}
|
||||
if (!result.success) onNotice('无法打开账号目录')
|
||||
}, [onNotice])
|
||||
|
||||
const disconnect = async (): Promise<void> => {
|
||||
const result = await window.api.disconnectDb()
|
||||
setConfirmDisconnect(false)
|
||||
if (!result.success) return onNotice(result.error || '断开连接失败')
|
||||
setStatus('unavailable')
|
||||
onConnectionChanged()
|
||||
onNotice('数据库连接已断开,微信原始数据未被修改')
|
||||
}
|
||||
const copyAccountDirectory = useCallback(async (): Promise<void> => {
|
||||
const accountRoot = selfInfo?.accountRoot
|
||||
if (!accountRoot) return onNotice('账号目录不可用')
|
||||
const result = await window.api.copyText(accountRoot)
|
||||
onNotice(result.success ? '账号目录已复制' : '复制账号目录失败')
|
||||
}, [onNotice, selfInfo?.accountRoot])
|
||||
|
||||
return {
|
||||
settings,
|
||||
pendingRoot,
|
||||
status,
|
||||
diagnostics,
|
||||
isTesting,
|
||||
lastVerifiedAt,
|
||||
confirmDisconnect,
|
||||
setConfirmDisconnect,
|
||||
chooseDirectory,
|
||||
applyDirectory,
|
||||
connectionStatus,
|
||||
checkState,
|
||||
isChecking: checkState.status === 'checking',
|
||||
lastCheckedLabel,
|
||||
testConnection,
|
||||
openAccountDirectory,
|
||||
disconnect
|
||||
copyAccountDirectory
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,19 +10,9 @@ export type SettingsCategoryId =
|
||||
| '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
|
||||
}
|
||||
|
||||
@@ -1,199 +1,65 @@
|
||||
import { AccountOverview } from '../account-database/AccountOverview'
|
||||
import { ConnectionHealthSection } from '../account-database/ConnectionHealthSection'
|
||||
import { LocalPrivacyNotice } from '../account-database/LocalPrivacyNotice'
|
||||
import { useAccountDatabaseController } from '../account-database/useAccountDatabaseController'
|
||||
import type { ConnectionOverviewStatus } from '../account-database/types'
|
||||
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>
|
||||
const STATUS_LABELS: Record<ConnectionOverviewStatus, string> = {
|
||||
checking: '正在检测',
|
||||
success: '连接正常',
|
||||
warning: '部分能力不可用',
|
||||
error: '连接异常',
|
||||
unavailable: '尚未连接'
|
||||
}
|
||||
|
||||
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'
|
||||
const controller = useAccountDatabaseController({ dbKey, dbReady, selfInfo, onNotice })
|
||||
return (
|
||||
<div className="settings-page">
|
||||
<header className="settings-page-header">
|
||||
<div>
|
||||
<h1>账号与数据库</h1>
|
||||
<p>管理当前微信账号以及本地数据库连接</p>
|
||||
<p>查看当前微信账号与本地数据库连接状态</p>
|
||||
</div>
|
||||
<span className={`settings-status-badge ${controller.status}`}>
|
||||
{controller.status === 'checking'
|
||||
? '正在验证'
|
||||
: connected
|
||||
? '连接正常'
|
||||
: controller.status === 'error'
|
||||
? '连接异常'
|
||||
: '尚未连接'}
|
||||
<span className={`settings-status-badge ${controller.connectionStatus}`}>
|
||||
{STATUS_LABELS[controller.connectionStatus]}
|
||||
</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>
|
||||
<LocalPrivacyNotice />
|
||||
<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>
|
||||
<AccountOverview
|
||||
selfInfo={selfInfo}
|
||||
connectionStatus={controller.connectionStatus}
|
||||
lastCheckedLabel={controller.lastCheckedLabel}
|
||||
isChecking={controller.isChecking}
|
||||
onCheck={() => void controller.testConnection()}
|
||||
onOpenDirectory={() => void controller.openAccountDirectory()}
|
||||
onCopyDirectory={() => void controller.copyAccountDirectory()}
|
||||
/>
|
||||
<h2 className="settings-section-heading">连接健康检查</h2>
|
||||
<ConnectionHealthSection
|
||||
diagnostics={controller.diagnostics}
|
||||
summary={
|
||||
controller.checkState.status === 'error' || controller.checkState.status === 'warning'
|
||||
? controller.checkState.message
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
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