feat: Add batch processing and upload functionality, including new backend APIs, logging system, SQLite database, and dedicated frontend pages.

This commit is contained in:
2026-02-01 02:53:37 +08:00
parent 94ba61528a
commit a605e46f2a
9 changed files with 953 additions and 99 deletions

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'
import { Trash2, Clock, Loader2, Save, RefreshCw, CheckCircle, XCircle, ToggleLeft, ToggleRight, AlertTriangle } from 'lucide-react'
import { useState, useEffect, useCallback } from 'react'
import { Trash2, Clock, Loader2, Save, RefreshCw, CheckCircle, XCircle, ToggleLeft, ToggleRight, AlertTriangle, ChevronLeft, ChevronRight, ScrollText } from 'lucide-react'
import { Card, CardHeader, CardTitle, CardContent, Button } from '../components/common'
interface CleanerStatus {
@@ -9,6 +9,14 @@ interface CleanerStatus {
last_clean_time: string
}
interface CleanerLogEntry {
timestamp: string
level: string
message: string
email?: string
module?: string
}
export default function Cleaner() {
const [loading, setLoading] = useState(true)
const [cleanEnabled, setCleanEnabled] = useState(false)
@@ -18,6 +26,14 @@ export default function Cleaner() {
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null)
const [status, setStatus] = useState<CleanerStatus | null>(null)
// 清理日志状态
const [logEntries, setLogEntries] = useState<CleanerLogEntry[]>([])
const [logPage, setLogPage] = useState(1)
const [logTotalPages, setLogTotalPages] = useState(0)
const [logTotal, setLogTotal] = useState(0)
const [logLoading, setLogLoading] = useState(false)
const logPageSize = 5
// 加载清理设置
const fetchCleanerSettings = async () => {
setLoading(true)
@@ -36,10 +52,37 @@ export default function Cleaner() {
}
}
// 加载清理日志
const fetchCleanerLogs = useCallback(async (page = 1) => {
setLogLoading(true)
try {
const params = new URLSearchParams({
module: 'cleaner',
page: String(page),
page_size: String(logPageSize),
})
const res = await fetch(`/api/logs/query?${params}`)
const data = await res.json()
if (data.code === 0 && data.data) {
setLogEntries(data.data.logs || [])
setLogTotalPages(data.data.total_pages || 0)
setLogTotal(data.data.total || 0)
}
} catch (error) {
console.error('Failed to fetch cleaner logs:', error)
} finally {
setLogLoading(false)
}
}, [logPageSize])
useEffect(() => {
fetchCleanerSettings()
}, [])
useEffect(() => {
fetchCleanerLogs(logPage)
}, [logPage, fetchCleanerLogs])
// 保存清理设置
const handleSaveCleanerSettings = async () => {
setSavingClean(true)
@@ -78,8 +121,9 @@ export default function Cleaner() {
const data = await res.json()
if (data.code === 0) {
setMessage({ type: 'success', text: data.data.message || '清理完成' })
// 刷新状态
// 刷新状态和日志
fetchCleanerSettings()
fetchCleanerLogs(logPage)
} else {
setMessage({ type: 'error', text: data.message || '清理失败' })
}
@@ -132,6 +176,21 @@ export default function Cleaner() {
}
}
// 日志级别样式
const levelColors: Record<string, string> = {
success: 'text-green-600 dark:text-green-400 bg-green-50 dark:bg-green-900/20',
error: 'text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20',
warning: 'text-yellow-600 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-900/20',
info: 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20',
}
const levelLabels: Record<string, string> = {
success: 'SUCCESS',
error: 'ERROR',
warning: 'WARN',
info: 'INFO',
}
if (loading) {
return (
<div className="flex items-center justify-center h-64">
@@ -326,6 +385,91 @@ export default function Cleaner() {
</CardContent>
</Card>
{/* 清理日志 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<ScrollText className="h-5 w-5 text-blue-500" />
</CardTitle>
<div className="flex items-center gap-2">
<span className="text-sm text-slate-500 dark:text-slate-400">
{logTotal}
</span>
<Button
variant="outline"
size="sm"
onClick={() => fetchCleanerLogs(logPage)}
icon={<RefreshCw className={`h-3 w-3 ${logLoading ? 'animate-spin' : ''}`} />}
>
</Button>
</div>
</CardHeader>
<CardContent>
{logLoading && logEntries.length === 0 ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-5 w-5 animate-spin text-slate-400" />
<span className="ml-2 text-sm text-slate-500">...</span>
</div>
) : logEntries.length === 0 ? (
<div className="text-center py-8 text-slate-400">
<ScrollText className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p className="text-sm"></p>
</div>
) : (
<div className="space-y-1">
{logEntries.map((log, i) => (
<div key={`${log.timestamp}-${i}`} className="flex items-start gap-3 text-sm py-2.5 border-b border-slate-100 dark:border-slate-800 last:border-0">
<span className="text-xs text-slate-400 flex-shrink-0 mt-0.5 font-mono whitespace-nowrap">
{new Date(log.timestamp).toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})}
</span>
<span className={`text-xs font-medium flex-shrink-0 mt-0.5 px-1.5 py-0.5 rounded ${levelColors[log.level] || 'text-slate-500 bg-slate-50 dark:bg-slate-800'}`}>
{levelLabels[log.level] || log.level?.toUpperCase()}
</span>
<span className="text-slate-700 dark:text-slate-300 break-all">
{log.message}
</span>
</div>
))}
</div>
)}
</CardContent>
{logTotalPages > 1 && (
<div className="flex-shrink-0 p-3 border-t border-slate-100 dark:border-slate-800 flex items-center justify-between">
<span className="text-sm text-slate-500 dark:text-slate-400">
{logPage} / {logTotalPages}
</span>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setLogPage(p => Math.max(1, p - 1))}
disabled={logPage <= 1}
icon={<ChevronLeft className="h-4 w-4" />}
>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setLogPage(p => Math.min(logTotalPages, p + 1))}
disabled={logPage >= logTotalPages}
icon={<ChevronRight className="h-4 w-4" />}
>
</Button>
</div>
</div>
)}
</Card>
{/* 说明信息 */}
<Card>
<CardContent className="py-4">
@@ -337,7 +481,7 @@ export default function Cleaner() {
<li> S2A "error"</li>
<li><strong></strong></li>
<li></li>
<li>"号池监控"</li>
<li>使</li>
<li>"保存设置"</li>
</ul>
</div>