From 3c5bb04d829f9c427e016bd926b16a342c4e4766 Mon Sep 17 00:00:00 2001 From: kyx236 Date: Fri, 30 Jan 2026 19:55:21 +0800 Subject: [PATCH] feat: Introduce owner management functionality with a new frontend list component and supporting backend API. --- backend/cmd/main.go | 69 +++++++++++ frontend/src/components/upload/OwnerList.tsx | 113 +++++++++++++++++-- 2 files changed, 175 insertions(+), 7 deletions(-) diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 9186dd7..f65fad3 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -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 { diff --git a/frontend/src/components/upload/OwnerList.tsx b/frontend/src/components/upload/OwnerList.tsx index 8ff8dac..8733d2d 100644 --- a/frontend/src/components/upload/OwnerList.tsx +++ b/frontend/src/components/upload/OwnerList.tsx @@ -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 = { 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 = { 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('') + const [selectedIds, setSelectedIds] = useState>(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) { + + {selectedIds.size > 0 && ( + + )} + 邮箱 Account ID 状态 @@ -185,19 +275,28 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) { {loading ? ( - + 加载中... ) : owners.length === 0 ? ( - + 暂无数据 ) : ( owners.map((owner) => ( - + + + + {owner.email} {owner.account_id ? `${owner.account_id.slice(0, 20)}...` : '-'}