diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 37fd683..2662667 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -133,6 +133,7 @@ func startServer(cfg *config.Config) { mux.HandleFunc("/api/db/owners/clear-used", api.CORS(handleClearUsedOwners)) // 清理已使用 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/ids", api.CORS(handleGetOwnerIDs)) // GET 获取所有ID(全选用) mux.HandleFunc("/api/db/owners/refetch-account-ids", api.CORS(api.HandleRefetchAccountIDs)) mux.HandleFunc("/api/upload/validate", api.CORS(api.HandleUploadValidate)) @@ -977,6 +978,31 @@ func handleBatchDeleteOwners(w http.ResponseWriter, r *http.Request) { }) } +// handleGetOwnerIDs GET /api/db/owners/ids?status=xxx - 获取所有符合条件的 owner ID(全选用) +func handleGetOwnerIDs(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + api.Error(w, http.StatusMethodNotAllowed, "仅支持 GET") + return + } + + if database.Instance == nil { + api.Error(w, http.StatusInternalServerError, "数据库未初始化") + return + } + + status := r.URL.Query().Get("status") + ids, err := database.Instance.GetTeamOwnerIDs(status) + if err != nil { + api.Error(w, http.StatusInternalServerError, fmt.Sprintf("查询失败: %v", err)) + return + } + + api.Success(w, map[string]interface{}{ + "ids": ids, + "total": len(ids), + }) +} + // handleRegisterTest POST /api/register/test - 测试注册流程 func handleRegisterTest(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { diff --git a/backend/internal/database/sqlite.go b/backend/internal/database/sqlite.go index 1772697..ba077b5 100644 --- a/backend/internal/database/sqlite.go +++ b/backend/internal/database/sqlite.go @@ -459,6 +459,33 @@ func (d *DB) UpdateOwnerAccountID(id int64, accountID string) error { return err } +// GetTeamOwnerIDs 获取所有符合条件的 owner ID(用于全选) +func (d *DB) GetTeamOwnerIDs(status string) ([]int64, error) { + query := "SELECT id FROM team_owners WHERE 1=1" + args := []interface{}{} + if status != "" { + query += " AND status = ?" + args = append(args, status) + } + query += " ORDER BY created_at DESC" + + rows, err := d.db.Query(query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + var ids []int64 + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + continue + } + ids = append(ids, id) + } + return ids, nil +} + // GetOwnerStats 获取统计 func (d *DB) GetOwnerStats() map[string]int { stats := map[string]int{ diff --git a/backend/internal/logger/logger.go b/backend/internal/logger/logger.go index 01d3cd1..07a3151 100644 --- a/backend/internal/logger/logger.go +++ b/backend/internal/logger/logger.go @@ -238,7 +238,38 @@ func GetLogs(limit int) []LogEntry { } // GetLogsByModule 按模块筛选日志并分页(最新的在前) +// 优先从内存读取,内存无数据时回退到数据库 func GetLogsByModule(module string, page, pageSize int) ([]LogEntry, int) { + logsMu.RLock() + // 检查内存中是否有该模块的日志 + hasMemoryLogs := false + for i := len(logs) - 1; i >= 0; i-- { + if logs[i].Module == module { + hasMemoryLogs = true + break + } + } + logsMu.RUnlock() + + // 内存中无数据,回退到数据库查询 + if !hasMemoryLogs && database.Instance != nil { + dbLogs, total, err := database.Instance.GetLogsByModule(module, page, pageSize) + if err == nil && total > 0 { + entries := make([]LogEntry, len(dbLogs)) + for i, dl := range dbLogs { + entries[i] = LogEntry{ + Timestamp: dl.Timestamp, + Level: dl.Level, + Message: dl.Message, + Email: dl.Email, + Module: dl.Module, + } + } + return entries, total + } + } + + // 从内存读取 logsMu.RLock() defer logsMu.RUnlock() @@ -283,7 +314,40 @@ func ClearLogs() { } // GetLogsByModuleAndLevel 按模块和级别筛选日志并分页(最新的在前) +// 优先从内存读取,内存无数据时回退到数据库 func GetLogsByModuleAndLevel(module, level string, page, pageSize int) ([]LogEntry, int) { + logsMu.RLock() + // 检查内存中是否有该模块+级别的日志 + hasMemoryLogs := false + for i := len(logs) - 1; i >= 0; i-- { + if logs[i].Module == module { + if level == "" || logs[i].Level == level { + hasMemoryLogs = true + break + } + } + } + logsMu.RUnlock() + + // 内存中无数据,回退到数据库查询 + if !hasMemoryLogs && database.Instance != nil { + dbLogs, total, err := database.Instance.GetLogsByModuleAndLevel(module, level, page, pageSize) + if err == nil && total > 0 { + entries := make([]LogEntry, len(dbLogs)) + for i, dl := range dbLogs { + entries[i] = LogEntry{ + Timestamp: dl.Timestamp, + Level: dl.Level, + Message: dl.Message, + Email: dl.Email, + Module: dl.Module, + } + } + return entries, total + } + } + + // 从内存读取 logsMu.RLock() defer logsMu.RUnlock() diff --git a/frontend/src/components/upload/OwnerList.tsx b/frontend/src/components/upload/OwnerList.tsx index 73c01ff..231bdff 100644 --- a/frontend/src/components/upload/OwnerList.tsx +++ b/frontend/src/components/upload/OwnerList.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from 'react' -import { Users, Trash2, RefreshCw, ChevronLeft, ChevronRight, Key, CheckSquare, Square, ShieldCheck, Settings, Clock } from 'lucide-react' +import { Users, Trash2, RefreshCw, ChevronLeft, ChevronRight, Key, CheckSquare, Square, MinusSquare, ShieldCheck, Settings, Clock } from 'lucide-react' import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../common' interface TeamOwner { @@ -58,6 +58,7 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) { const [page, setPage] = useState(0) const [filter, setFilter] = useState('') const [selectedIds, setSelectedIds] = useState>(new Set()) + const [selectAllMode, setSelectAllMode] = useState(false) // 是否全选所有页 const [deleting, setDeleting] = useState(false) const limit = 20 @@ -93,8 +94,10 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) { useEffect(() => { loadOwners() - // 清除选择 - setSelectedIds(new Set()) + // 清除选择(全选所有页模式除外) + if (!selectAllMode) { + setSelectedIds(new Set()) + } }, [page, filter]) // 加载封禁检查配置 @@ -295,6 +298,7 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) { // 选择逻辑 const toggleSelect = (id: number) => { + setSelectAllMode(false) const newSet = new Set(selectedIds) if (newSet.has(id)) { newSet.delete(id) @@ -305,14 +309,40 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) { } const toggleSelectAll = () => { - if (selectedIds.size === owners.length) { + if (selectedIds.size === owners.length && !selectAllMode) { + // 当前页已全选,取消全选 setSelectedIds(new Set()) } else { + // 选中当前页所有 + setSelectAllMode(false) setSelectedIds(new Set(owners.map(o => o.id))) } } - const isAllSelected = owners.length > 0 && selectedIds.size === owners.length + // 全选所有页 + const handleSelectAllPages = async () => { + try { + const params = new URLSearchParams() + if (filter) params.set('status', filter) + const res = await fetch(`/api/db/owners/ids?${params}`) + const data = await res.json() + if (data.code === 0 && data.data.ids) { + setSelectedIds(new Set(data.data.ids)) + setSelectAllMode(true) + } + } catch (e) { + console.error('Failed to select all:', e) + } + } + + // 取消全选 + const handleClearSelection = () => { + setSelectedIds(new Set()) + setSelectAllMode(false) + } + + const isAllSelected = owners.length > 0 && selectedIds.size === owners.length && !selectAllMode + const isPagePartialSelected = selectedIds.size > 0 && owners.some(o => selectedIds.has(o.id)) && !isAllSelected && !selectAllMode const totalPages = Math.ceil(total / limit) @@ -478,14 +508,60 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) { )} + {/* 全选提示条 */} + {selectedIds.size > 0 && ( +
+ {selectAllMode ? ( + <> + + 已选择全部 {selectedIds.size} 个{filter ? ` "${statusLabels[filter] || filter}" 状态的` : ''}母号 + + + + ) : isAllSelected && total > owners.length ? ( + <> + + 已选择当前页全部 {owners.length} 个母号 + + + + ) : ( + + 已选择 {selectedIds.size} 个母号 + {total > owners.length && ( + + )} + + )} +
+ )}