feat: Introduce owner management functionality with a new frontend list component and supporting backend API.

This commit is contained in:
2026-01-30 19:55:21 +08:00
parent 119b24efb2
commit 3c5bb04d82
2 changed files with 175 additions and 7 deletions

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'
import { Users, Trash2, RefreshCw, ChevronLeft, ChevronRight, Key } from 'lucide-react'
import { Users, Trash2, RefreshCw, ChevronLeft, ChevronRight, Key, CheckSquare, Square } from 'lucide-react'
import { Card, CardHeader, CardTitle, CardContent, Button } from '../common'
interface TeamOwner {
@@ -14,12 +14,16 @@ 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',
processing: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400',
invalid: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400',
}
const statusLabels: Record<string, string> = {
valid: '有效',
registered: '已注册',
pooled: '已入库',
processing: 'processing',
invalid: '无效',
}
interface OwnerListProps {
@@ -32,6 +36,8 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
const [loading, setLoading] = useState(false)
const [page, setPage] = useState(0)
const [filter, setFilter] = useState<string>('')
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set())
const [deleting, setDeleting] = useState(false)
const limit = 20
const loadOwners = async () => {
@@ -60,16 +66,57 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
useEffect(() => {
loadOwners()
// 清除选择
setSelectedIds(new Set())
}, [page, filter])
// 单个删除
const handleDelete = async (id: number) => {
if (!confirm('确认删除此账号?')) return
try {
await fetch(`/api/db/owners/${id}`, { method: 'DELETE' })
loadOwners()
onStatsChange?.()
const res = await fetch(`/api/db/owners/delete/${id}`, { method: 'POST' })
const data = await res.json()
if (data.code === 0) {
loadOwners()
onStatsChange?.()
} else {
alert(data.message || '删除失败')
}
} catch (e) {
console.error('Failed to delete:', e)
alert('删除失败')
}
}
// 批量删除
const handleBatchDelete = async () => {
if (selectedIds.size === 0) {
alert('请先选择要删除的账号')
return
}
if (!confirm(`确认删除选中的 ${selectedIds.size} 个账号?`)) return
setDeleting(true)
try {
const res = await fetch('/api/db/owners/batch-delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids: Array.from(selectedIds) }),
})
const data = await res.json()
if (data.code === 0) {
alert(data.data.message)
setSelectedIds(new Set())
loadOwners()
onStatsChange?.()
} else {
alert(data.message || '删除失败')
}
} catch (e) {
console.error('Failed to batch delete:', e)
alert('删除失败')
} finally {
setDeleting(false)
}
}
@@ -108,6 +155,27 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
}
}
// 选择逻辑
const toggleSelect = (id: number) => {
const newSet = new Set(selectedIds)
if (newSet.has(id)) {
newSet.delete(id)
} else {
newSet.add(id)
}
setSelectedIds(newSet)
}
const toggleSelectAll = () => {
if (selectedIds.size === owners.length) {
setSelectedIds(new Set())
} else {
setSelectedIds(new Set(owners.map(o => o.id)))
}
}
const isAllSelected = owners.length > 0 && selectedIds.size === owners.length
const totalPages = Math.ceil(total / limit)
const formatTime = (ts: string) => {
@@ -144,6 +212,7 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
<option value="valid"></option>
<option value="registered"></option>
<option value="pooled"></option>
<option value="invalid"></option>
</select>
<Button variant="ghost" size="sm" onClick={loadOwners} icon={<RefreshCw className="h-4 w-4" />}>
@@ -158,6 +227,18 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
>
{refetching ? '获取中...' : '重新获取ID'}
</Button>
{selectedIds.size > 0 && (
<Button
variant="ghost"
size="sm"
onClick={handleBatchDelete}
disabled={deleting}
icon={<Trash2 className="h-4 w-4" />}
className="text-orange-500 hover:text-orange-600"
>
{deleting ? '删除中...' : `删除选中 (${selectedIds.size})`}
</Button>
)}
<Button
variant="ghost"
size="sm"
@@ -175,6 +256,15 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
<table className="w-full text-sm">
<thead className="bg-slate-50 dark:bg-slate-800 sticky top-0">
<tr>
<th className="text-center p-3 w-10">
<button onClick={toggleSelectAll} className="hover:text-blue-500">
{isAllSelected ? (
<CheckSquare className="h-4 w-4 text-blue-500" />
) : (
<Square className="h-4 w-4" />
)}
</button>
</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">Account ID</th>
<th className="text-left p-3 font-medium text-slate-600 dark:text-slate-400"></th>
@@ -185,19 +275,28 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
<tbody>
{loading ? (
<tr>
<td colSpan={5} className="text-center py-8 text-slate-500">
<td colSpan={6} 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 colSpan={6} 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">
<tr key={owner.id} className={`border-t border-slate-100 dark:border-slate-800 hover:bg-slate-50 dark:hover:bg-slate-800/50 ${selectedIds.has(owner.id) ? 'bg-blue-50 dark:bg-blue-900/20' : ''}`}>
<td className="p-3 text-center">
<button onClick={() => toggleSelect(owner.id)} className="hover:text-blue-500">
{selectedIds.has(owner.id) ? (
<CheckSquare className="h-4 w-4 text-blue-500" />
) : (
<Square className="h-4 w-4" />
)}
</button>
</td>
<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 ? `${owner.account_id.slice(0, 20)}...` : '-'}