feat: Implement initial full-stack application structure including frontend pages, components, hooks, API integration, and backend services for account pooling and management.
This commit is contained in:
218
frontend/src/components/upload/OwnerList.tsx
Normal file
218
frontend/src/components/upload/OwnerList.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Users, Trash2, RefreshCw, ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
import { Card, CardHeader, CardTitle, CardContent, Button } from '../common'
|
||||
|
||||
interface TeamOwner {
|
||||
id: number
|
||||
email: string
|
||||
account_id: string
|
||||
status: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
interface OwnerListProps {
|
||||
apiBase?: string
|
||||
}
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
valid: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
|
||||
registered: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400',
|
||||
pooled: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400',
|
||||
}
|
||||
|
||||
const statusLabels: Record<string, string> = {
|
||||
valid: '有效',
|
||||
registered: '已注册',
|
||||
pooled: '已入库',
|
||||
}
|
||||
|
||||
export default function OwnerList({ apiBase = 'http://localhost:8088' }: OwnerListProps) {
|
||||
const [owners, setOwners] = useState<TeamOwner[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [page, setPage] = useState(0)
|
||||
const [filter, setFilter] = useState<string>('')
|
||||
const limit = 20
|
||||
|
||||
const loadOwners = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
limit: String(limit),
|
||||
offset: String(page * limit),
|
||||
})
|
||||
if (filter) {
|
||||
params.set('status', filter)
|
||||
}
|
||||
|
||||
const res = await fetch(`${apiBase}/api/db/owners?${params}`)
|
||||
const data = await res.json()
|
||||
if (data.code === 0) {
|
||||
setOwners(data.data.owners || [])
|
||||
setTotal(data.data.total || 0)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load owners:', e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadOwners()
|
||||
}, [page, filter])
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!confirm('确认删除此账号?')) return
|
||||
try {
|
||||
await fetch(`${apiBase}/api/db/owners/${id}`, { method: 'DELETE' })
|
||||
loadOwners()
|
||||
} catch (e) {
|
||||
console.error('Failed to delete:', e)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClearAll = async () => {
|
||||
if (!confirm('确认清空所有账号?此操作不可恢复!')) return
|
||||
try {
|
||||
await fetch(`${apiBase}/api/db/owners/clear`, { method: 'POST' })
|
||||
loadOwners()
|
||||
} catch (e) {
|
||||
console.error('Failed to clear:', e)
|
||||
}
|
||||
}
|
||||
|
||||
const totalPages = Math.ceil(total / limit)
|
||||
|
||||
const formatTime = (ts: string) => {
|
||||
try {
|
||||
return new Date(ts).toLocaleString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="h-full flex flex-col">
|
||||
<CardHeader className="flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Users className="h-5 w-5" />
|
||||
母号列表 ({total})
|
||||
</CardTitle>
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
className="px-2 py-1 text-sm rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800"
|
||||
value={filter}
|
||||
onChange={(e) => {
|
||||
setFilter(e.target.value)
|
||||
setPage(0)
|
||||
}}
|
||||
>
|
||||
<option value="">全部状态</option>
|
||||
<option value="valid">有效</option>
|
||||
<option value="registered">已注册</option>
|
||||
<option value="pooled">已入库</option>
|
||||
</select>
|
||||
<Button variant="ghost" size="sm" onClick={loadOwners} icon={<RefreshCw className="h-4 w-4" />}>
|
||||
刷新
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleClearAll}
|
||||
icon={<Trash2 className="h-4 w-4" />}
|
||||
className="text-red-500 hover:text-red-600"
|
||||
>
|
||||
清空
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 overflow-hidden p-0">
|
||||
<div className="h-full overflow-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-slate-50 dark:bg-slate-800 sticky top-0">
|
||||
<tr>
|
||||
<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">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-center p-3 font-medium text-slate-600 dark:text-slate-400">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="text-center py-8 text-slate-500">
|
||||
加载中...
|
||||
</td>
|
||||
</tr>
|
||||
) : owners.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="text-center py-8 text-slate-500">
|
||||
暂无数据
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
owners.map((owner) => (
|
||||
<tr key={owner.id} className="border-t border-slate-100 dark:border-slate-800 hover:bg-slate-50 dark:hover:bg-slate-800/50">
|
||||
<td className="p-3 text-slate-900 dark:text-slate-100">{owner.email}</td>
|
||||
<td className="p-3 font-mono text-xs text-slate-500">{owner.account_id?.slice(0, 20)}...</td>
|
||||
<td className="p-3">
|
||||
<span className={`px-2 py-0.5 rounded-full text-xs ${statusColors[owner.status] || 'bg-slate-100 text-slate-700'}`}>
|
||||
{statusLabels[owner.status] || owner.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="p-3 text-slate-500 text-xs">{formatTime(owner.created_at)}</td>
|
||||
<td className="p-3 text-center">
|
||||
<button
|
||||
onClick={() => handleDelete(owner.id)}
|
||||
className="text-red-400 hover:text-red-500"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
{/* Pagination */}
|
||||
{totalPages > 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">
|
||||
第 {page + 1} / {totalPages} 页
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPage((p) => Math.max(0, p - 1))}
|
||||
disabled={page === 0}
|
||||
icon={<ChevronLeft className="h-4 w-4" />}
|
||||
>
|
||||
上一页
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
|
||||
disabled={page >= totalPages - 1}
|
||||
icon={<ChevronRight className="h-4 w-4" />}
|
||||
>
|
||||
下一页
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user