forked from boos/BoosAPI
feat: 使用日志新增自动刷新功能
This commit is contained in:
@@ -877,6 +877,12 @@ body {
|
|||||||
background: color-mix(in srgb, var(--color-primary) 22%, var(--color-bg-card));
|
background: color-mix(in srgb, var(--color-primary) 22%, var(--color-bg-card));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-ghost-active {
|
||||||
|
background: color-mix(in srgb, var(--color-primary) 22%, var(--color-bg-card));
|
||||||
|
border-color: var(--color-primary) !important;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
.btn-link {
|
.btn-link {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
@@ -219,6 +219,7 @@ export default function ProxyLogs() {
|
|||||||
const [detailById, setDetailById] = useState<Record<number, ProxyLogDetailState>>({});
|
const [detailById, setDetailById] = useState<Record<number, ProxyLogDetailState>>({});
|
||||||
const [showFilters, setShowFilters] = useState(false);
|
const [showFilters, setShowFilters] = useState(false);
|
||||||
const [sites, setSites] = useState<Array<{ id: number; name: string; status?: string | null }>>([]);
|
const [sites, setSites] = useState<Array<{ id: number; name: string; status?: string | null }>>([]);
|
||||||
|
const [autoRefresh, setAutoRefresh] = useState(false);
|
||||||
const isMobile = useIsMobile(768);
|
const isMobile = useIsMobile(768);
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const loadSeq = useRef(0);
|
const loadSeq = useRef(0);
|
||||||
@@ -320,7 +321,7 @@ export default function ProxyLogs() {
|
|||||||
return siteOptions.find((option) => option.value === String(siteFilter))?.label || `站点 #${siteFilter}`;
|
return siteOptions.find((option) => option.value === String(siteFilter))?.label || `站点 #${siteFilter}`;
|
||||||
}, [siteFilter, siteOptions]);
|
}, [siteFilter, siteOptions]);
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async (silent = false) => {
|
||||||
const seq = ++loadSeq.current;
|
const seq = ++loadSeq.current;
|
||||||
if (hasInvalidTimeRange) {
|
if (hasInvalidTimeRange) {
|
||||||
setLogs([]);
|
setLogs([]);
|
||||||
@@ -329,7 +330,7 @@ export default function ProxyLogs() {
|
|||||||
if (seq === loadSeq.current) setLoading(false);
|
if (seq === loadSeq.current) setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLoading(true);
|
if (!silent) setLoading(true);
|
||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
limit: pageSize,
|
limit: pageSize,
|
||||||
@@ -347,7 +348,7 @@ export default function ProxyLogs() {
|
|||||||
setSummary(data.summary || EMPTY_SUMMARY);
|
setSummary(data.summary || EMPTY_SUMMARY);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (seq !== loadSeq.current) return;
|
if (seq !== loadSeq.current) return;
|
||||||
toast.error(e.message || '加载日志失败');
|
if (!silent) toast.error(e.message || '加载日志失败');
|
||||||
} finally {
|
} finally {
|
||||||
if (seq === loadSeq.current) setLoading(false);
|
if (seq === loadSeq.current) setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -357,6 +358,12 @@ export default function ProxyLogs() {
|
|||||||
void load();
|
void load();
|
||||||
}, [load]);
|
}, [load]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!autoRefresh) return;
|
||||||
|
const timer = setInterval(() => { void load(true); }, 2000);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, [autoRefresh, load]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (page <= totalPages) return;
|
if (page <= totalPages) return;
|
||||||
setPage(totalPages);
|
setPage(totalPages);
|
||||||
@@ -504,12 +511,25 @@ export default function ProxyLogs() {
|
|||||||
{summary.totalTokensAll.toLocaleString()} tokens
|
{summary.totalTokensAll.toLocaleString()} tokens
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={load} disabled={loading} className="btn btn-ghost" style={{ border: '1px solid var(--color-border)', padding: '6px 14px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<svg width="14" height="14" fill="none" viewBox="0 0 24 24" stroke="currentColor" style={{ animation: loading ? 'spin 1s linear infinite' : 'none' }}>
|
<button
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
onClick={() => setAutoRefresh((v) => !v)}
|
||||||
</svg>
|
className={`btn btn-ghost${autoRefresh ? ' btn-ghost-active' : ''}`}
|
||||||
{loading ? '加载中...' : '刷新'}
|
style={{ border: '1px solid var(--color-border)', padding: '6px 14px' }}
|
||||||
</button>
|
title={autoRefresh ? '关闭自动刷新' : '开启自动刷新(每2秒)'}
|
||||||
|
>
|
||||||
|
<svg width="14" height="14" fill="none" viewBox="0 0 24 24" stroke="currentColor" style={{ animation: autoRefresh ? 'spin 1s linear infinite' : 'none' }}>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||||
|
</svg>
|
||||||
|
{autoRefresh ? '自动刷新中' : '自动刷新'}
|
||||||
|
</button>
|
||||||
|
<button onClick={() => load()} disabled={loading} className="btn btn-ghost" style={{ border: '1px solid var(--color-border)', padding: '6px 14px' }}>
|
||||||
|
<svg width="14" height="14" fill="none" viewBox="0 0 24 24" stroke="currentColor" style={{ animation: loading ? 'spin 1s linear infinite' : 'none' }}>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||||
|
</svg>
|
||||||
|
{loading ? '加载中...' : '刷新'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user