优化网络与系统检测为缓存优先,避免页面长时间停留在检测中。
将网络/IP 路由拆分并加入去重缓存,系统检测改为线程执行并支持强制刷新,前端改为先展示缓存快照后按需手动重测,降低阻塞与重复探测开销。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'
|
||||
import { apiFetch, apiUrl } from './api'
|
||||
import { saveWorkspaceSnapshot } from './workspace'
|
||||
import { saveWorkspaceSnapshot, loadWorkspaceSnapshot } from './workspace'
|
||||
import ProxySettings from './ProxySettings'
|
||||
import { type FlowStep } from './components/worker/WorkerFlowStrip'
|
||||
import LivePulse from './components/ui/LivePulse'
|
||||
@@ -308,74 +308,44 @@ export default function NetworkDashboard() {
|
||||
const [data, setData] = useState<NetworkPayload | null>(null)
|
||||
const [ipProfile, setIpProfile] = useState<IpProfilePayload | null>(null)
|
||||
const [ipProfileErr, setIpProfileErr] = useState<string | null>(null)
|
||||
const [ipProfileLoading, setIpProfileLoading] = useState(true)
|
||||
const [ipProfileLoading, setIpProfileLoading] = useState(false)
|
||||
const [ipQuality, setIpQuality] = useState<IpQualityPayload | null>(null)
|
||||
const [ipQualityErr, setIpQualityErr] = useState<string | null>(null)
|
||||
const [ipQualityLoading, setIpQualityLoading] = useState(true)
|
||||
const [ipQualityLoading, setIpQualityLoading] = useState(false)
|
||||
const [proxyCfg, setProxyCfg] = useState<ProxyCfg>({ enabled: false, http: '', socks: '' })
|
||||
const [err, setErr] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const [hasCachedData, setHasCachedData] = useState(false)
|
||||
const { leak: webrtcLeak } = useWebRtcLeak(true)
|
||||
|
||||
const loadIpProfile = useCallback(async () => {
|
||||
setIpProfileErr(null)
|
||||
setIpProfileLoading(true)
|
||||
try {
|
||||
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone || ''
|
||||
const lang = navigator.language || ''
|
||||
const q = new URLSearchParams({ tz, lang })
|
||||
if (webrtcLeak != null) q.set('webrtc_leak', webrtcLeak ? '1' : '0')
|
||||
const res = await apiFetch(apiUrl(`/ip_profile?${q.toString()}`))
|
||||
const json = (await res.json()) as IpProfilePayload
|
||||
if (!res.ok) {
|
||||
setIpProfileErr(json.error || `IP 画像请求失败 ${res.status}`)
|
||||
setIpProfile(null)
|
||||
return
|
||||
}
|
||||
setIpProfile(json)
|
||||
saveWorkspaceSnapshot('ip_profile', json)
|
||||
} catch (e) {
|
||||
setIpProfileErr(e instanceof Error ? e.message : String(e))
|
||||
setIpProfile(null)
|
||||
} finally {
|
||||
setIpProfileLoading(false)
|
||||
}
|
||||
}, [webrtcLeak])
|
||||
|
||||
const loadIpQuality = useCallback(async () => {
|
||||
setIpQualityErr(null)
|
||||
setIpQualityLoading(true)
|
||||
try {
|
||||
const res = await apiFetch(apiUrl('/ip_quality'))
|
||||
const json = (await res.json()) as IpQualityPayload
|
||||
if (!res.ok) {
|
||||
setIpQualityErr(json.error || `IP 质量请求失败 ${res.status}`)
|
||||
setIpQuality(null)
|
||||
return
|
||||
}
|
||||
setIpQuality(json)
|
||||
saveWorkspaceSnapshot('ip_quality', json)
|
||||
} catch (e) {
|
||||
setIpQualityErr(e instanceof Error ? e.message : String(e))
|
||||
setIpQuality(null)
|
||||
} finally {
|
||||
setIpQualityLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const load = useCallback(async (opts?: { silent?: boolean }) => {
|
||||
const load = useCallback(async (opts?: { silent?: boolean; forceRefresh?: boolean }) => {
|
||||
const silent = opts?.silent ?? false
|
||||
const forceRefresh = opts?.forceRefresh ?? false
|
||||
if (!forceRefresh) return
|
||||
setErr(null)
|
||||
if (!silent) setLoading(true)
|
||||
else setRefreshing(true)
|
||||
setIpProfileLoading(true)
|
||||
setIpQualityLoading(true)
|
||||
try {
|
||||
const [netRes, proxyRes] = await Promise.all([
|
||||
apiFetch(apiUrl('/network_status')),
|
||||
const q = '?force_refresh=1'
|
||||
const [netRes, proxyRes, profileRes, qualityRes] = await Promise.all([
|
||||
apiFetch(apiUrl(`/network_status${q}`)),
|
||||
apiFetch(apiUrl('/proxy_config')),
|
||||
apiFetch(
|
||||
apiUrl(
|
||||
`/ip_profile?${new URLSearchParams({
|
||||
tz: Intl.DateTimeFormat().resolvedOptions().timeZone || '',
|
||||
lang: navigator.language || '',
|
||||
...(webrtcLeak != null ? { webrtc_leak: webrtcLeak ? '1' : '0' } : {}),
|
||||
force_refresh: '1',
|
||||
}).toString()}`,
|
||||
),
|
||||
),
|
||||
apiFetch(apiUrl('/ip_quality?force_refresh=1')),
|
||||
])
|
||||
void loadIpProfile()
|
||||
void loadIpQuality()
|
||||
|
||||
const json = (await netRes.json()) as NetworkPayload & { error?: string }
|
||||
if (!netRes.ok) {
|
||||
setErr(json.error || `请求失败 ${netRes.status}`)
|
||||
@@ -383,7 +353,29 @@ export default function NetworkDashboard() {
|
||||
} else {
|
||||
setData(json)
|
||||
saveWorkspaceSnapshot('network_status', json)
|
||||
setHasCachedData(true)
|
||||
}
|
||||
|
||||
const profileJson = (await profileRes.json()) as IpProfilePayload
|
||||
if (!profileRes.ok) {
|
||||
setIpProfileErr(profileJson.error || `IP 画像请求失败 ${profileRes.status}`)
|
||||
setIpProfile(null)
|
||||
} else {
|
||||
setIpProfile(profileJson)
|
||||
setIpProfileErr(null)
|
||||
saveWorkspaceSnapshot('ip_profile', profileJson)
|
||||
}
|
||||
|
||||
const qualityJson = (await qualityRes.json()) as IpQualityPayload
|
||||
if (!qualityRes.ok) {
|
||||
setIpQualityErr(qualityJson.error || `IP 质量请求失败 ${qualityRes.status}`)
|
||||
setIpQuality(null)
|
||||
} else {
|
||||
setIpQuality(qualityJson)
|
||||
setIpQualityErr(null)
|
||||
saveWorkspaceSnapshot('ip_quality', qualityJson)
|
||||
}
|
||||
|
||||
if (proxyRes.ok) {
|
||||
const proxyJson = (await proxyRes.json()) as ProxyCfg
|
||||
setProxyCfg({
|
||||
@@ -398,22 +390,44 @@ export default function NetworkDashboard() {
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setRefreshing(false)
|
||||
setIpProfileLoading(false)
|
||||
setIpQualityLoading(false)
|
||||
}
|
||||
}, [loadIpProfile, loadIpQuality])
|
||||
}, [webrtcLeak])
|
||||
|
||||
useEffect(() => {
|
||||
void load()
|
||||
}, [load])
|
||||
|
||||
useEffect(() => {
|
||||
const id = window.setInterval(() => void load({ silent: true }), 45000)
|
||||
return () => clearInterval(id)
|
||||
}, [load])
|
||||
|
||||
useEffect(() => {
|
||||
if (webrtcLeak == null) return
|
||||
void loadIpProfile()
|
||||
}, [webrtcLeak, loadIpProfile])
|
||||
let cancelled = false
|
||||
void (async () => {
|
||||
const [net, profile, quality] = await Promise.all([
|
||||
loadWorkspaceSnapshot<NetworkPayload>('network_status'),
|
||||
loadWorkspaceSnapshot<IpProfilePayload>('ip_profile'),
|
||||
loadWorkspaceSnapshot<IpQualityPayload>('ip_quality'),
|
||||
])
|
||||
if (cancelled) return
|
||||
if (net) {
|
||||
setData(net)
|
||||
setHasCachedData(true)
|
||||
}
|
||||
if (profile) setIpProfile(profile)
|
||||
if (quality) setIpQuality(quality)
|
||||
try {
|
||||
const proxyRes = await apiFetch(apiUrl('/proxy_config'))
|
||||
if (!cancelled && proxyRes.ok) {
|
||||
const proxyJson = (await proxyRes.json()) as ProxyCfg
|
||||
setProxyCfg({
|
||||
enabled: !!proxyJson.enabled,
|
||||
http: proxyJson.http || '',
|
||||
socks: proxyJson.socks || '',
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
/* 代理配置读取失败不影响展示缓存结果 */
|
||||
}
|
||||
})()
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
const view = data ?? DEFAULT_NETWORK
|
||||
const status = overallStatus(view, loading)
|
||||
@@ -479,7 +493,15 @@ export default function NetworkDashboard() {
|
||||
: ['net-routes__pill--no', '境内外出口可能未区分']
|
||||
|
||||
const statusLabel =
|
||||
loading ? '检测中' : status === 'ready' ? '网络就绪' : status === 'warn' ? '需关注' : '出口异常'
|
||||
loading || refreshing
|
||||
? '检测中'
|
||||
: !hasCachedData
|
||||
? '待检测'
|
||||
: status === 'ready'
|
||||
? '网络就绪'
|
||||
: status === 'warn'
|
||||
? '需关注'
|
||||
: '出口异常'
|
||||
|
||||
const pulseTone = status === 'ready' ? 'live' : status === 'warn' ? 'warn' : 'idle'
|
||||
|
||||
@@ -517,10 +539,10 @@ export default function NetworkDashboard() {
|
||||
type="button"
|
||||
className="btn btn-primary btn--sm net-summary__btn"
|
||||
disabled={loading || refreshing || ipProfileLoading || ipQualityLoading}
|
||||
onClick={() => void load({ silent: !!data })}
|
||||
onClick={() => void load({ silent: hasCachedData, forceRefresh: true })}
|
||||
>
|
||||
<IconRefresh className={loading || refreshing ? 'is-spinning' : ''} />
|
||||
{loading || refreshing ? '检测中…' : '重新检测'}
|
||||
{loading || refreshing ? '检测中…' : hasCachedData ? '重新检测' : '开始检测'}
|
||||
</button>
|
||||
<button type="button" className="btn btn-secondary btn--sm" onClick={openProxySettings}>
|
||||
Windows 代理
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { apiFetch, apiUrl } from './api'
|
||||
import { saveWorkspaceSnapshot } from './workspace'
|
||||
import { saveWorkspaceSnapshot, loadWorkspaceSnapshot } from './workspace'
|
||||
import MetricBar, { toneFromPercent } from './components/ui/MetricBar'
|
||||
|
||||
type Metrics = {
|
||||
@@ -81,10 +81,18 @@ function InfoRow({ label, value }: { label: string; value: string }) {
|
||||
export default function SystemMetrics() {
|
||||
const [data, setData] = useState<Metrics | null>(null)
|
||||
const [err, setErr] = useState<string | null>(null)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const [hasCachedData, setHasCachedData] = useState(false)
|
||||
const inFlightRef = useRef(false)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const load = useCallback(async (opts?: { forceRefresh?: boolean }) => {
|
||||
const forceRefresh = opts?.forceRefresh ?? false
|
||||
if (!forceRefresh) return
|
||||
if (inFlightRef.current) return
|
||||
inFlightRef.current = true
|
||||
setRefreshing(true)
|
||||
try {
|
||||
const res = await apiFetch(apiUrl('/system_metrics'))
|
||||
const res = await apiFetch(apiUrl('/system_metrics?force_refresh=1'))
|
||||
const json = (await res.json()) as Metrics
|
||||
if (!res.ok) {
|
||||
setErr(json.error || `HTTP ${res.status}`)
|
||||
@@ -92,17 +100,30 @@ export default function SystemMetrics() {
|
||||
}
|
||||
setErr(null)
|
||||
setData(json)
|
||||
setHasCachedData(true)
|
||||
saveWorkspaceSnapshot('system_metrics', json)
|
||||
} catch (e) {
|
||||
setErr(e instanceof Error ? e.message : String(e))
|
||||
} finally {
|
||||
setRefreshing(false)
|
||||
inFlightRef.current = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
void load()
|
||||
const id = window.setInterval(() => void load(), 3000)
|
||||
return () => clearInterval(id)
|
||||
}, [load])
|
||||
let cancelled = false
|
||||
void (async () => {
|
||||
const cached = await loadWorkspaceSnapshot<Metrics>('system_metrics')
|
||||
if (cancelled) return
|
||||
if (cached) {
|
||||
setData(cached)
|
||||
setHasCachedData(true)
|
||||
}
|
||||
})()
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
const sys = data?.system
|
||||
const hw = data?.hardware
|
||||
@@ -122,7 +143,17 @@ export default function SystemMetrics() {
|
||||
</h2>
|
||||
<p className="card__desc">本机操作系统、处理器、显卡与资源占用</p>
|
||||
</div>
|
||||
<span className="metrics-uptime">⏱ 运行 {fmtUptime(sys?.boot_time_unix)}</span>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span className="metrics-uptime">⏱ 运行 {fmtUptime(sys?.boot_time_unix)}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn--sm"
|
||||
disabled={refreshing}
|
||||
onClick={() => void load({ forceRefresh: true })}
|
||||
>
|
||||
{refreshing ? '检测中…' : hasCachedData ? '重新检测' : '开始检测'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{err ? (
|
||||
@@ -209,7 +240,7 @@ export default function SystemMetrics() {
|
||||
<div className="metrics-disks__title">磁盘空间</div>
|
||||
<ul className="metrics-disk-list">
|
||||
{data.disks.slice(0, 8).map((d) => (
|
||||
<li key={d.mountpoint}>
|
||||
<li key={`${d.mountpoint || 'unknown'}-${d.device || 'dev'}`}>
|
||||
<span className="metrics-disk-list__mp">
|
||||
{d.mountpoint}
|
||||
{d.fstype ? ` (${d.fstype})` : ''}
|
||||
|
||||
@@ -25,6 +25,18 @@ export function saveWorkspaceSnapshot(key: string, data: unknown): void {
|
||||
void getWorkspace()?.saveSnapshot(key, data)
|
||||
}
|
||||
|
||||
export async function loadWorkspaceSnapshot<T>(key: string): Promise<T | null> {
|
||||
const ws = getWorkspace()
|
||||
if (!ws) return null
|
||||
try {
|
||||
const dash = await ws.getDashboard()
|
||||
const item = dash.snapshots[key]
|
||||
return item?.data != null ? (item.data as T) : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function logUserAction(action: string, nav?: string, detail?: unknown): void {
|
||||
void getWorkspace()?.logUserAction({ action, nav, detail })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user