feat: Introduce owner management functionality with a new frontend list component and supporting backend API.
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -110,6 +111,8 @@ func startServer(cfg *config.Config) {
|
||||
mux.HandleFunc("/api/db/owners", api.CORS(handleGetOwners))
|
||||
mux.HandleFunc("/api/db/owners/stats", api.CORS(handleGetOwnerStats))
|
||||
mux.HandleFunc("/api/db/owners/clear", api.CORS(handleClearOwners))
|
||||
mux.HandleFunc("/api/db/owners/delete/", api.CORS(handleDeleteOwner)) // DELETE /api/db/owners/delete/{id}
|
||||
mux.HandleFunc("/api/db/owners/batch-delete", api.CORS(handleBatchDeleteOwners)) // POST 批量删除
|
||||
mux.HandleFunc("/api/db/owners/refetch-account-ids", api.CORS(api.HandleRefetchAccountIDs))
|
||||
mux.HandleFunc("/api/upload/validate", api.CORS(api.HandleUploadValidate))
|
||||
|
||||
@@ -642,6 +645,72 @@ func handleClearOwners(w http.ResponseWriter, r *http.Request) {
|
||||
api.Success(w, map[string]string{"message": "已清空"})
|
||||
}
|
||||
|
||||
// handleDeleteOwner DELETE /api/db/owners/delete/{id} - 删除单个 owner
|
||||
func handleDeleteOwner(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete && r.Method != http.MethodPost {
|
||||
api.Error(w, http.StatusMethodNotAllowed, "仅支持 DELETE/POST")
|
||||
return
|
||||
}
|
||||
|
||||
if database.Instance == nil {
|
||||
api.Error(w, http.StatusInternalServerError, "数据库未初始化")
|
||||
return
|
||||
}
|
||||
|
||||
// 从 URL 提取 ID: /api/db/owners/delete/{id}
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/db/owners/delete/")
|
||||
id, err := strconv.Atoi(path)
|
||||
if err != nil {
|
||||
api.Error(w, http.StatusBadRequest, "无效的 ID")
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.Instance.DeleteTeamOwner(int64(id)); err != nil {
|
||||
api.Error(w, http.StatusInternalServerError, fmt.Sprintf("删除失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
api.Success(w, map[string]string{"message": "已删除"})
|
||||
}
|
||||
|
||||
// handleBatchDeleteOwners POST /api/db/owners/batch-delete - 批量删除 owners
|
||||
func handleBatchDeleteOwners(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
api.Error(w, http.StatusMethodNotAllowed, "仅支持 POST")
|
||||
return
|
||||
}
|
||||
|
||||
if database.Instance == nil {
|
||||
api.Error(w, http.StatusInternalServerError, "数据库未初始化")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
IDs []int `json:"ids"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
api.Error(w, http.StatusBadRequest, "请求格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.IDs) == 0 {
|
||||
api.Error(w, http.StatusBadRequest, "请选择要删除的账号")
|
||||
return
|
||||
}
|
||||
|
||||
deleted := 0
|
||||
for _, id := range req.IDs {
|
||||
if err := database.Instance.DeleteTeamOwner(int64(id)); err == nil {
|
||||
deleted++
|
||||
}
|
||||
}
|
||||
|
||||
api.Success(w, map[string]interface{}{
|
||||
"message": fmt.Sprintf("已删除 %d 个账号", deleted),
|
||||
"deleted": deleted,
|
||||
})
|
||||
}
|
||||
|
||||
// handleRegisterTest POST /api/register/test - 测试注册流程
|
||||
func handleRegisterTest(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
|
||||
@@ -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' })
|
||||
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)}...` : '-'}
|
||||
|
||||
Reference in New Issue
Block a user