feat: Add batch processing and upload functionality, including new backend APIs, logging system, SQLite database, and dedicated frontend pages.
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user