feat: Implement core backend infrastructure including configuration management, SQLite database with team owners and app settings, and initial owner-related APIs and frontend components.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Users, Trash2, RefreshCw, ChevronLeft, ChevronRight, Key, CheckSquare, Square } from 'lucide-react'
|
||||
import { Card, CardHeader, CardTitle, CardContent, Button } from '../common'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Users, Trash2, RefreshCw, ChevronLeft, ChevronRight, Key, CheckSquare, Square, ShieldCheck, Settings, Clock } from 'lucide-react'
|
||||
import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../common'
|
||||
|
||||
interface TeamOwner {
|
||||
id: number
|
||||
@@ -8,6 +8,25 @@ interface TeamOwner {
|
||||
account_id: string
|
||||
status: string
|
||||
created_at: string
|
||||
last_checked_at?: string
|
||||
}
|
||||
|
||||
interface BanCheckTaskState {
|
||||
running: boolean
|
||||
started_at: string
|
||||
total: number
|
||||
checked: number
|
||||
banned: number
|
||||
valid: number
|
||||
failed: number
|
||||
}
|
||||
|
||||
interface BanCheckSettings {
|
||||
enabled: boolean
|
||||
interval: number
|
||||
check_hours: number
|
||||
service_running: boolean
|
||||
task_state: BanCheckTaskState
|
||||
}
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
@@ -42,6 +61,12 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
const limit = 20
|
||||
|
||||
// 封禁检查相关状态
|
||||
const [banCheckSettings, setBanCheckSettings] = useState<BanCheckSettings | null>(null)
|
||||
const [showBanCheckSettings, setShowBanCheckSettings] = useState(false)
|
||||
const [banCheckRunning, setBanCheckRunning] = useState(false)
|
||||
const [checkHours, setCheckHours] = useState(24)
|
||||
|
||||
const loadOwners = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
@@ -72,6 +97,99 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
|
||||
setSelectedIds(new Set())
|
||||
}, [page, filter])
|
||||
|
||||
// 加载封禁检查配置
|
||||
const loadBanCheckSettings = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch('/api/db/owners/ban-check/settings')
|
||||
const data = await res.json()
|
||||
if (data.code === 0) {
|
||||
setBanCheckSettings(data.data)
|
||||
setCheckHours(data.data.check_hours || 24)
|
||||
setBanCheckRunning(data.data.task_state?.running || false)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load ban check settings:', e)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
loadBanCheckSettings()
|
||||
// 轮询检查状态
|
||||
const interval = setInterval(() => {
|
||||
if (banCheckRunning) {
|
||||
loadBanCheckSettings()
|
||||
}
|
||||
}, 2000)
|
||||
return () => clearInterval(interval)
|
||||
}, [loadBanCheckSettings, banCheckRunning])
|
||||
|
||||
// 手动触发封禁检查
|
||||
const handleBanCheck = async (forceCheck = false) => {
|
||||
if (banCheckRunning) return
|
||||
setBanCheckRunning(true)
|
||||
try {
|
||||
const body: { ids?: number[], force_check?: boolean } = {}
|
||||
if (selectedIds.size > 0) {
|
||||
body.ids = Array.from(selectedIds)
|
||||
}
|
||||
if (forceCheck) {
|
||||
body.force_check = true
|
||||
}
|
||||
const res = await fetch('/api/db/owners/ban-check', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.code === 0) {
|
||||
// 开始轮询状态
|
||||
const pollInterval = setInterval(async () => {
|
||||
const statusRes = await fetch('/api/db/owners/ban-check/status')
|
||||
const statusData = await statusRes.json()
|
||||
if (statusData.code === 0) {
|
||||
if (!statusData.data.running) {
|
||||
clearInterval(pollInterval)
|
||||
setBanCheckRunning(false)
|
||||
loadOwners()
|
||||
onStatsChange?.()
|
||||
loadBanCheckSettings()
|
||||
}
|
||||
}
|
||||
}, 2000)
|
||||
} else {
|
||||
alert(data.message || '启动检查失败')
|
||||
setBanCheckRunning(false)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to start ban check:', e)
|
||||
alert('启动检查失败')
|
||||
setBanCheckRunning(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存封禁检查配置
|
||||
const handleSaveBanCheckSettings = async (enabled: boolean) => {
|
||||
try {
|
||||
const res = await fetch('/api/db/owners/ban-check/settings', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
enabled,
|
||||
check_hours: checkHours,
|
||||
}),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.code === 0) {
|
||||
loadBanCheckSettings()
|
||||
} else {
|
||||
alert(data.message || '保存失败')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to save ban check settings:', e)
|
||||
alert('保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 单个删除
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!confirm('确认删除此账号?')) return
|
||||
@@ -247,6 +365,29 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
|
||||
>
|
||||
{refetching ? '获取中...' : '重新获取ID'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleBanCheck(false)}
|
||||
disabled={banCheckRunning}
|
||||
icon={<ShieldCheck className={`h-4 w-4 ${banCheckRunning ? 'animate-pulse' : ''}`} />}
|
||||
className="text-emerald-500 hover:text-emerald-600"
|
||||
>
|
||||
{banCheckRunning
|
||||
? `检查中 (${banCheckSettings?.task_state?.checked || 0}/${banCheckSettings?.task_state?.total || 0})`
|
||||
: selectedIds.size > 0
|
||||
? `检查封禁 (${selectedIds.size})`
|
||||
: '检查封禁'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowBanCheckSettings(!showBanCheckSettings)}
|
||||
icon={<Settings className="h-4 w-4" />}
|
||||
className={showBanCheckSettings ? 'text-blue-500' : 'text-slate-500 hover:text-slate-600'}
|
||||
>
|
||||
定期检查
|
||||
</Button>
|
||||
{selectedIds.size > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -280,6 +421,62 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
{/* 封禁检查设置面板 */}
|
||||
{showBanCheckSettings && (
|
||||
<div className="px-4 py-3 bg-slate-50 dark:bg-slate-800/50 border-b border-slate-200 dark:border-slate-700">
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm text-slate-600 dark:text-slate-400">定期检查:</label>
|
||||
<button
|
||||
onClick={() => handleSaveBanCheckSettings(!banCheckSettings?.enabled)}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
banCheckSettings?.enabled ? 'bg-emerald-500' : 'bg-slate-300 dark:bg-slate-600'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
banCheckSettings?.enabled ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="h-4 w-4 text-slate-400" />
|
||||
<label className="text-sm text-slate-600 dark:text-slate-400">检查间隔:</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={checkHours}
|
||||
onChange={(e) => setCheckHours(parseInt(e.target.value) || 24)}
|
||||
className="w-20 h-8 text-sm"
|
||||
min={1}
|
||||
max={168}
|
||||
/>
|
||||
<span className="text-sm text-slate-500">小时</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleSaveBanCheckSettings(banCheckSettings?.enabled || false)}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-auto text-xs text-slate-500">
|
||||
{banCheckSettings?.task_state?.running ? (
|
||||
<span className="text-emerald-500">
|
||||
检查中: {banCheckSettings.task_state.checked}/{banCheckSettings.task_state.total}
|
||||
(有效: {banCheckSettings.task_state.valid}, 封禁: {banCheckSettings.task_state.banned})
|
||||
</span>
|
||||
) : banCheckSettings?.enabled ? (
|
||||
<span>服务已启用</span>
|
||||
) : (
|
||||
<span>服务未启用</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CardContent className="flex-1 overflow-hidden p-0">
|
||||
<div className="h-full overflow-auto">
|
||||
<table className="w-full text-sm">
|
||||
@@ -298,19 +495,20 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
|
||||
<th className="text-left p-3 font-medium text-slate-600 dark:text-slate-400">Account ID</th>
|
||||
<th className="text-left p-3 font-medium text-slate-600 dark:text-slate-400">状态</th>
|
||||
<th className="text-left p-3 font-medium text-slate-600 dark:text-slate-400">创建时间</th>
|
||||
<th className="text-left p-3 font-medium text-slate-600 dark:text-slate-400">上次检查</th>
|
||||
<th className="text-center p-3 font-medium text-slate-600 dark:text-slate-400">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="text-center py-8 text-slate-500">
|
||||
<td colSpan={7} className="text-center py-8 text-slate-500">
|
||||
加载中...
|
||||
</td>
|
||||
</tr>
|
||||
) : owners.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="text-center py-8 text-slate-500">
|
||||
<td colSpan={7} className="text-center py-8 text-slate-500">
|
||||
暂无数据
|
||||
</td>
|
||||
</tr>
|
||||
@@ -336,6 +534,9 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
|
||||
</span>
|
||||
</td>
|
||||
<td className="p-3 text-slate-500 text-xs">{formatTime(owner.created_at)}</td>
|
||||
<td className="p-3 text-slate-500 text-xs">
|
||||
{owner.last_checked_at ? formatTime(owner.last_checked_at) : '-'}
|
||||
</td>
|
||||
<td className="p-3 text-center">
|
||||
<button
|
||||
onClick={() => handleDelete(owner.id)}
|
||||
|
||||
Reference in New Issue
Block a user