chore: initial project import

This commit is contained in:
justme committed 2026-10-04 21:46:39 +08:00
commit 26620707a0
347 files changed
+56172

No files matched your search

+79
View File
@@ -0,0 +1,79 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { ApiError } from './api/client'
export interface QueryState<T> {
data: T | undefined
error: ApiError | Error | undefined
loading: boolean
reload: () => void
}
/** 取数:依赖变化时重新取;可选轮询。重新取数时保留上一次的数据,避免界面闪烁。 */
export function useQuery<T>(fetcher: () => Promise<T>, deps: unknown[], intervalMs?: number): QueryState<T> {
const [data, setData] = useState<T>()
const [error, setError] = useState<ApiError | Error>()
const [loading, setLoading] = useState(true)
const [tick, setTick] = useState(0)
const fetcherRef = useRef(fetcher)
fetcherRef.current = fetcher
useEffect(() => {
let alive = true
setLoading(true)
fetcherRef
.current()
.then((value) => {
if (!alive) return
setData(value)
setError(undefined)
})
.catch((e: unknown) => {
if (alive) setError(e instanceof Error ? e : new Error(String(e)))
})
.finally(() => {
if (alive) setLoading(false)
})
return () => {
alive = false
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...deps, tick])
useEffect(() => {
if (!intervalMs) return
const id = window.setInterval(() => setTick((t) => t + 1), intervalMs)
return () => window.clearInterval(id)
}, [intervalMs])
const reload = useCallback(() => setTick((t) => t + 1), [])
return { data, error, loading, reload }
}
export type Theme = 'light' | 'dark'
export function useTheme(): [Theme, () => void] {
const [theme, setTheme] = useState<Theme>(() => (document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light'))
const toggle = useCallback(() => {
const next: Theme = theme === 'dark' ? 'light' : 'dark'
document.documentElement.dataset.theme = next
try {
localStorage.setItem('monitor-theme', next)
} catch {
// 存储不可用时只在本次会话生效
}
setTheme(next)
}, [theme])
return [theme, toggle]
}
/** 首屏之前调用:按保存的选择或系统偏好设置主题。 */
export function applyStoredTheme() {
let theme: string | null = null
try {
theme = localStorage.getItem('monitor-theme')
} catch {
// 忽略
}
const dark = theme ? theme === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches
document.documentElement.dataset.theme = dark ? 'dark' : 'light'
}