feat: Implement S2A account cleaner management and a global application configuration context, with supporting backend API.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { Terminal, Play, Pause, Trash2, ChevronDown } from 'lucide-react'
|
||||
|
||||
interface LogEntry {
|
||||
@@ -43,11 +43,34 @@ export default function LiveLogViewer({
|
||||
const eventSourceRef = useRef<EventSource | null>(null)
|
||||
const pausedLogsRef = useRef<LogEntry[]>([])
|
||||
|
||||
// 连接 SSE
|
||||
const connect = useCallback(() => {
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close()
|
||||
const isPausedRef = useRef(isPaused)
|
||||
isPausedRef.current = isPaused
|
||||
|
||||
// 加载历史日志
|
||||
const loadHistoryLogs = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/logs')
|
||||
const data = await res.json()
|
||||
if (data.code === 0 && Array.isArray(data.data)) {
|
||||
// 转换为 LogEntry 格式
|
||||
const historyLogs: LogEntry[] = data.data.map((log: { level: string; message: string; timestamp: string; email?: string; module?: string }) => ({
|
||||
type: 'log',
|
||||
level: log.level || 'info',
|
||||
message: log.message || '',
|
||||
timestamp: log.timestamp ? new Date(log.timestamp).toLocaleTimeString('zh-CN') : '',
|
||||
email: log.email,
|
||||
module: log.module || 'system',
|
||||
}))
|
||||
setLogs(historyLogs.slice(-maxLogs))
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load history logs:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时先加载历史日志,再连接 SSE
|
||||
useEffect(() => {
|
||||
loadHistoryLogs()
|
||||
|
||||
const es = new EventSource('/api/logs/stream')
|
||||
eventSourceRef.current = es
|
||||
@@ -61,12 +84,11 @@ export default function LiveLogViewer({
|
||||
const data = JSON.parse(event.data) as LogEntry
|
||||
|
||||
if (data.type === 'connected') {
|
||||
console.log('SSE connected:', data)
|
||||
return
|
||||
}
|
||||
|
||||
if (data.type === 'log') {
|
||||
if (isPaused) {
|
||||
if (isPausedRef.current) {
|
||||
pausedLogsRef.current.push(data)
|
||||
} else {
|
||||
setLogs((prev) => {
|
||||
@@ -82,20 +104,13 @@ export default function LiveLogViewer({
|
||||
|
||||
es.onerror = () => {
|
||||
setIsConnected(false)
|
||||
// 自动重连
|
||||
setTimeout(connect, 3000)
|
||||
}
|
||||
}, [isPaused, maxLogs])
|
||||
|
||||
// 组件挂载时连接
|
||||
useEffect(() => {
|
||||
connect()
|
||||
return () => {
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close()
|
||||
}
|
||||
es.close()
|
||||
}
|
||||
}, [connect])
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
// 自动滚动
|
||||
useEffect(() => {
|
||||
|
||||
@@ -61,6 +61,11 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
|
||||
refreshConfig()
|
||||
}, [refreshConfig])
|
||||
|
||||
// 当站点名称变化时,更新浏览器标签页标题
|
||||
useEffect(() => {
|
||||
document.title = siteName
|
||||
}, [siteName])
|
||||
|
||||
// Update S2A client when config changes and auto-test connection
|
||||
useEffect(() => {
|
||||
if (config.s2a.apiBase && config.s2a.adminKey) {
|
||||
|
||||
@@ -108,6 +108,30 @@ export default function Cleaner() {
|
||||
}
|
||||
}
|
||||
|
||||
// 计算下次清理时间
|
||||
const getNextCleanTime = (): string => {
|
||||
if (!cleanEnabled) return '未启用'
|
||||
if (!status?.last_clean_time || status.last_clean_time === '0001-01-01T00:00:00Z') {
|
||||
return '即将执行'
|
||||
}
|
||||
try {
|
||||
const lastTime = new Date(status.last_clean_time)
|
||||
const nextTime = new Date(lastTime.getTime() + cleanInterval * 1000)
|
||||
// 如果下次时间已过,说明即将执行
|
||||
if (nextTime <= new Date()) {
|
||||
return '即将执行'
|
||||
}
|
||||
return nextTime.toLocaleString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
} catch {
|
||||
return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
@@ -193,6 +217,9 @@ export default function Cleaner() {
|
||||
<p className="text-2xl font-bold text-slate-900 dark:text-slate-100">
|
||||
{formatInterval(cleanInterval)}
|
||||
</p>
|
||||
<p className="text-xs text-slate-400 dark:text-slate-500 mt-1">
|
||||
下次清理: {getNextCleanTime()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 rounded-xl bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center">
|
||||
<Clock className="h-6 w-6 text-blue-500" />
|
||||
|
||||
Reference in New Issue
Block a user