Spaces:
Running
Running
| 'use client'; | |
| import { useState, useEffect } from 'react'; | |
| import { X, Users, Search, Shield, Key, Crown, CheckCircle2, Calendar, Trash2, Receipt } from 'lucide-react'; | |
| import { api, AdminUserItem, AdminOrderItem } from '@/lib/api'; | |
| import { useAuthStore } from '@/store/authStore'; | |
| interface AdminPanelProps { | |
| isOpen: boolean; | |
| onClose: () => void; | |
| } | |
| export default function AdminPanel({ isOpen, onClose }: AdminPanelProps) { | |
| const { token } = useAuthStore(); | |
| const [users, setUsers] = useState<AdminUserItem[]>([]); | |
| const [loading, setLoading] = useState(true); | |
| const [error, setError] = useState<string | null>(null); | |
| const [searchQuery, setSearchQuery] = useState(''); | |
| const [activeTab, setActiveTab] = useState<'all' | 'vip' | 'normal'>('all'); | |
| // 操作状态 | |
| const [actionLoading, setActionLoading] = useState<number | null>(null); // userId | |
| const [successMsg, setSuccessMsg] = useState<string | null>(null); | |
| // 订单查看状态 | |
| const [viewingOrdersUser, setViewingOrdersUser] = useState<{ id: number; name: string } | null>(null); | |
| const [userOrders, setUserOrders] = useState<AdminOrderItem[]>([]); | |
| const [ordersLoading, setOrdersLoading] = useState(false); | |
| const fetchUsers = async () => { | |
| if (!token) return; | |
| setLoading(true); | |
| try { | |
| const data = await api.adminGetUsers(token); | |
| setUsers(data); | |
| setError(null); | |
| } catch (err: any) { | |
| setError(err.message || '获取用户列表失败'); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| useEffect(() => { | |
| if (isOpen) { | |
| fetchUsers(); | |
| } | |
| }, [isOpen, token]); | |
| // 修改密码 | |
| const handleUpdatePassword = async (userId: number, username: string) => { | |
| const newPassword = window.prompt(`请输入用户 [${username}] 的新密码:`); | |
| if (!newPassword || !token) return; | |
| setActionLoading(userId); | |
| try { | |
| await api.adminUpdatePassword(userId, newPassword, token); | |
| setSuccessMsg(`用户 ${username} 密码修改成功`); | |
| setTimeout(() => setSuccessMsg(null), 3000); | |
| } catch (err: any) { | |
| alert('修改失败: ' + err.message); | |
| } finally { | |
| setActionLoading(null); | |
| } | |
| }; | |
| // 充值会员 | |
| const handleUpdateVip = async (userId: number, username: string) => { | |
| const monthsStr = window.prompt(`请输入为用户 [${username}] 充值的月数:`, '1'); | |
| if (!monthsStr || !token) return; | |
| const months = parseInt(monthsStr); | |
| if (isNaN(months) || months <= 0) return alert('请输入有效数字'); | |
| setActionLoading(userId); | |
| try { | |
| await api.adminUpdateVip(userId, months, token); | |
| setSuccessMsg(`用户 ${username} 充值成功 (+${months}个月)`); | |
| fetchUsers(); | |
| setTimeout(() => setSuccessMsg(null), 3000); | |
| } catch (err: any) { | |
| alert('充值失败: ' + err.message); | |
| } finally { | |
| setActionLoading(null); | |
| } | |
| }; | |
| // 删除用户 | |
| const handleDeleteUser = async (userId: number, username: string) => { | |
| if (!window.confirm(`⚠️ 警告:确定要彻底删除用户 [${username}] 吗?\n\n此操作将清除该用户的所有数据(VIP、会话、使用记录),且无法撤销。`)) { | |
| return; | |
| } | |
| if (!token) return; | |
| setActionLoading(userId); | |
| try { | |
| await api.adminDeleteUser(userId, token); | |
| setSuccessMsg(`用户 ${username} 已被彻底删除`); | |
| fetchUsers(); | |
| setTimeout(() => setSuccessMsg(null), 3000); | |
| } catch (err: any) { | |
| alert('删除失败: ' + err.message); | |
| } finally { | |
| setActionLoading(null); | |
| } | |
| }; | |
| // 获取并查看订单 | |
| const handleViewOrders = async (userId: number, username: string) => { | |
| setViewingOrdersUser({ id: userId, name: username }); | |
| setOrdersLoading(true); | |
| try { | |
| const data = await api.adminUserOrders(userId, token!); | |
| setUserOrders(data); | |
| } catch (err: any) { | |
| alert('获取订单失败: ' + err.message); | |
| setViewingOrdersUser(null); | |
| } finally { | |
| setOrdersLoading(false); | |
| } | |
| }; | |
| const handleToggleOrderStatus = async (orderId: string, currentStatus: string) => { | |
| if (!token) return; | |
| const newStatus = currentStatus === 'paid' ? 'pending' : 'paid'; | |
| try { | |
| await api.adminUpdateOrderStatus(orderId, newStatus, token); | |
| // 本地状态更新 | |
| setUserOrders(prev => prev.map(o => | |
| o.order_id === orderId | |
| ? { ...o, status: newStatus, paid_at: newStatus === 'paid' ? new Date().toLocaleString() : null } | |
| : o | |
| )); | |
| } catch (err: any) { | |
| alert('切换状态失败: ' + err.message); | |
| } | |
| }; | |
| if (!isOpen) return null; | |
| const filteredUsers = users.filter(u => { | |
| const matchesSearch = u.username.toLowerCase().includes(searchQuery.toLowerCase()); | |
| const matchesTab = activeTab === 'all' || | |
| (activeTab === 'vip' && u.is_vip) || | |
| (activeTab === 'normal' && !u.is_vip); | |
| return matchesSearch && matchesTab; | |
| }); | |
| return ( | |
| <div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/90 backdrop-blur-xl p-0 md:p-2"> | |
| <div className="bg-[#0B0D17] w-full h-full md:max-w-[98vw] md:h-[96vh] md:rounded-[2rem] border border-white/10 shadow-2xl flex flex-col overflow-hidden animate-in fade-in zoom-in-95 duration-500"> | |
| {/* Header */} | |
| <div className="px-6 py-4 border-b border-white/5 flex items-center justify-between bg-gradient-to-r from-[#141726] to-[#0B0D17]"> | |
| <div className="flex items-center gap-3"> | |
| <div className="w-10 h-10 bg-indigo-500/10 rounded-xl flex items-center justify-center border border-indigo-500/20"> | |
| <Shield className="text-indigo-400" size={24} /> | |
| </div> | |
| <div> | |
| <h2 className="text-xl font-bold text-white tracking-tight"> | |
| 管理后台 | |
| </h2> | |
| <p className="text-[10px] text-gray-500 font-bold mt-0.5 tracking-wider">系统用户管理与权限控制</p> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-4"> | |
| {successMsg && ( | |
| <div className="hidden md:flex items-center gap-2 px-4 py-1.5 bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 rounded-xl text-xs font-bold animate-in slide-in-from-right-4"> | |
| <CheckCircle2 size={16} /> | |
| {successMsg} | |
| </div> | |
| )} | |
| <button | |
| onClick={onClose} | |
| className="w-10 h-10 flex items-center justify-center text-gray-400 hover:text-white hover:bg-white/10 rounded-full transition-all border border-white/5" | |
| > | |
| <X size={20} /> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Toolbar */} | |
| <div className="px-6 py-3 bg-[#0F111A] border-b border-white/5 flex items-center gap-6"> | |
| <div className="flex items-center gap-1 p-1 bg-black/40 rounded-xl border border-white/5 shrink-0"> | |
| {(['all', 'vip', 'normal'] as const).map((tab) => ( | |
| <button | |
| key={tab} | |
| onClick={() => setActiveTab(tab)} | |
| className={`px-4 py-1.5 text-xs font-bold rounded-lg transition-all duration-200 ${activeTab === tab ? 'bg-indigo-600 text-white shadow-lg' : 'text-gray-500 hover:text-gray-300'}`} | |
| > | |
| {tab === 'all' ? '全部' : tab === 'vip' ? '会员' : '普通'} | |
| </button> | |
| ))} | |
| </div> | |
| <div className="relative flex-1"> | |
| <Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500" size={16} /> | |
| <input | |
| type="text" | |
| placeholder="通过用户名搜索系统内所有注册用户..." | |
| className="w-full bg-black/40 border border-white/5 rounded-xl py-2 pl-10 pr-4 text-xs text-gray-200 focus:outline-none focus:ring-1 focus:ring-indigo-500/50 transition-all placeholder:text-gray-600 font-medium" | |
| value={searchQuery} | |
| onChange={(e) => setSearchQuery(e.target.value)} | |
| /> | |
| </div> | |
| </div> | |
| {/* Content */} | |
| <div className="flex-1 overflow-auto p-0 md:px-6 md:py-2 bg-[#0B0D17]"> | |
| {loading ? ( | |
| <div className="h-full flex flex-col items-center justify-center gap-4"> | |
| <div className="w-12 h-12 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin"></div> | |
| <p className="text-gray-500 text-xs font-bold tracking-widest">数据同步中...</p> | |
| </div> | |
| ) : ( | |
| <div> | |
| <table className="w-full border-separate border-spacing-y-1"> | |
| <thead className="sticky top-0 z-10"> | |
| <tr className="text-left text-[11px] font-bold text-gray-600 px-6"> | |
| <th className="pb-2 pl-4 w-auto">用户详情</th> | |
| <th className="pb-2 w-auto text-center px-4">会员状态</th> | |
| <th className="pb-2 w-auto text-center px-3">支付记录</th> | |
| <th className="pb-2 w-auto text-center px-3">最近登录</th> | |
| <th className="pb-2 w-auto text-center px-3">注册日期</th> | |
| <th className="pb-2 pr-4 w-auto text-right">管理操作</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {filteredUsers.length === 0 ? ( | |
| <tr> | |
| <td colSpan={6} className="py-20 text-center opacity-20"> | |
| <Users size={60} className="mx-auto mb-4" /> | |
| <p className="text-sm font-bold">无记录</p> | |
| </td> | |
| </tr> | |
| ) : ( | |
| filteredUsers.map((user) => ( | |
| <tr | |
| key={user.user_id} | |
| className="group bg-white/[0.015] hover:bg-white/[0.035] transition-all rounded-xl border border-white/5 cursor-pointer" | |
| onClick={() => handleViewOrders(user.user_id, user.username)} | |
| > | |
| <td className="py-2.5 pl-4 rounded-l-xl"> | |
| <div className="flex items-center gap-3"> | |
| <div className="w-8 h-8 bg-indigo-500/10 rounded-lg flex items-center justify-center font-bold text-indigo-400 text-xs border border-indigo-500/10"> | |
| {user.username[0].toUpperCase()} | |
| </div> | |
| <div className="flex flex-col"> | |
| <span className="text-gray-200 font-bold text-sm whitespace-nowrap">{user.username}</span> | |
| <span className="text-[9px] text-gray-600 font-bold uppercase tracking-tighter">UID: {user.user_id}</span> | |
| </div> | |
| </div> | |
| </td> | |
| <td className="py-2.5 text-center px-4"> | |
| {user.is_vip ? ( | |
| <div className="inline-flex items-center gap-3"> | |
| <span className="inline-flex items-center gap-1 text-yellow-500 text-[9px] font-bold px-2 py-0.5 bg-yellow-500/10 rounded-full border border-yellow-500/20"> | |
| <Crown size={10} /> | |
| 会员 | |
| </span> | |
| <div className="flex flex-col items-center gap-0.5"> | |
| <span className="text-[9px] text-gray-600 font-mono">{user.vip_expire_at?.split(' ')[0].replace(/-/g, '/') || ''}</span> | |
| <span className="text-[8px] text-gray-600 font-mono">{user.vip_expire_at?.split(' ')[1] || ''}</span> | |
| </div> | |
| </div> | |
| ) : ( | |
| <span className="inline-flex items-center text-gray-600 text-[9px] font-bold px-2 py-0.5 bg-white/5 rounded-full border border-white/5"> | |
| 普通 | |
| </span> | |
| )} | |
| </td> | |
| <td className="py-2.5 text-center px-3"> | |
| {user.has_payment ? ( | |
| <span className="inline-flex items-center gap-1 text-emerald-500 text-[9px] font-bold px-2 py-0.5 bg-emerald-500/10 rounded-full border border-emerald-500/20"> | |
| <CheckCircle2 size={10} /> | |
| 有 | |
| </span> | |
| ) : ( | |
| <span className="inline-flex items-center text-gray-600 text-[9px] font-bold px-2 py-0.5 bg-white/5 rounded-full border border-white/5"> | |
| 无 | |
| </span> | |
| )} | |
| </td> | |
| <td className="py-2.5 text-center px-3"> | |
| {user.last_login ? ( | |
| <div className="flex flex-col items-center gap-0.5"> | |
| <span className="text-[9px] text-gray-400 font-mono">{user.last_login.split(' ')[0].replace(/-/g, '/') || ''}</span> | |
| <span className="text-[8px] text-gray-600 font-mono">{user.last_login.split(' ')[1] || ''}</span> | |
| </div> | |
| ) : ( | |
| <span className="text-[9px] text-gray-600">从未登录</span> | |
| )} | |
| </td> | |
| <td className="py-2.5 text-center px-3"> | |
| <div className="flex flex-col items-center gap-0.5"> | |
| <span className="text-[9px] text-gray-400 font-mono">{user.created_at.split(' ')[0].replace(/-/g, '/') || ''}</span> | |
| <span className="text-[8px] text-gray-600 font-mono">{user.created_at.split(' ')[1] || ''}</span> | |
| </div> | |
| </td> | |
| <td className="py-2.5 pr-4 rounded-r-xl text-right" onClick={(e) => e.stopPropagation()}> | |
| <div className="flex items-center justify-end gap-2"> | |
| <button | |
| onClick={() => handleDeleteUser(user.user_id, user.username)} | |
| disabled={actionLoading === user.user_id} | |
| className="p-1 px-2.5 bg-red-500/5 text-red-400/60 hover:text-red-400 hover:bg-red-500/10 border border-white/5 hover:border-red-500/20 rounded-lg text-[10px] font-bold transition-all disabled:opacity-30 group/del" | |
| title="删除用户" | |
| > | |
| <Trash2 size={12} className="opacity-50 group-hover/del:opacity-100" /> | |
| </button> | |
| <button | |
| onClick={() => handleUpdatePassword(user.user_id, user.username)} | |
| disabled={actionLoading === user.user_id} | |
| className="px-3 py-1 bg-indigo-500/5 text-gray-400 hover:text-indigo-400 hover:bg-indigo-500/10 border border-white/5 hover:border-indigo-500/20 rounded-lg text-[10px] font-bold transition-all disabled:opacity-30" | |
| > | |
| 改密 | |
| </button> | |
| <button | |
| onClick={() => handleUpdateVip(user.user_id, user.username)} | |
| disabled={actionLoading === user.user_id} | |
| className="px-3 py-1 bg-yellow-500/5 text-gray-400 hover:text-yellow-500 hover:bg-yellow-500/10 border border-white/5 hover:border-yellow-500/20 rounded-lg text-[10px] font-bold transition-all disabled:opacity-30" | |
| > | |
| 续费 | |
| </button> | |
| </div> | |
| </td> | |
| </tr> | |
| )).reverse() | |
| )} | |
| </tbody> | |
| </table> | |
| </div> | |
| )} | |
| </div> | |
| {/* Footer */} | |
| <div className="px-6 py-3 bg-[#0B0D17] border-t border-white/5 flex items-center justify-between text-[10px] text-gray-600 font-bold uppercase tracking-wider"> | |
| <div className="flex items-center gap-6"> | |
| <span className="flex items-center gap-1.5"> | |
| <div className="w-1.2 h-1.2 bg-indigo-500 rounded-full animate-pulse"></div> | |
| 用户: {users.length} | |
| </span> | |
| <span className="flex items-center gap-1.5"> | |
| <div className="w-1.2 h-1.2 bg-yellow-500 rounded-full animate-pulse"></div> | |
| 会员: {users.filter(u => u.is_vip).length} | |
| </span> | |
| </div> | |
| <div className="flex items-center gap-1.5 opacity-30"> | |
| <Shield size={10} /> | |
| 管理控制台 | |
| </div> | |
| </div> | |
| {/* 订单详情弹窗 */} | |
| {viewingOrdersUser && ( | |
| <div className="fixed inset-0 z-[110] flex items-center justify-center bg-black/60 backdrop-blur-sm p-2 md:p-4 animate-in fade-in duration-200"> | |
| <div className="bg-[#121421] w-[98vw] h-[95vh] md:w-[90vw] md:h-[90vh] rounded-2xl border border-white/10 shadow-2xl flex flex-col overflow-hidden animate-in zoom-in-95 duration-200"> | |
| <div className="px-6 py-4 border-b border-white/5 flex items-center justify-between bg-gradient-to-r from-[#1A1D2E] to-[#121421]"> | |
| <div className="flex items-center gap-3"> | |
| <div className="w-8 h-8 bg-yellow-500/10 rounded-lg flex items-center justify-center text-yellow-500 border border-yellow-500/10"> | |
| <Receipt size={18} /> | |
| </div> | |
| <div> | |
| <h3 className="text-base font-bold text-white">支付记录</h3> | |
| <p className="text-[10px] text-gray-500 font-bold uppercase tracking-tight">用户: {viewingOrdersUser.name} (UID: {viewingOrdersUser.id})</p> | |
| </div> | |
| </div> | |
| <button | |
| onClick={() => setViewingOrdersUser(null)} | |
| className="w-8 h-8 flex items-center justify-center text-gray-400 hover:text-white hover:bg-white/10 rounded-full transition-all" | |
| > | |
| <X size={18} /> | |
| </button> | |
| </div> | |
| <div className="flex-1 overflow-auto p-6"> | |
| {ordersLoading ? ( | |
| <div className="py-20 flex flex-col items-center justify-center gap-3"> | |
| <div className="w-8 h-8 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin"></div> | |
| <p className="text-gray-500 text-[10px] font-bold tracking-widest uppercase">正在读取记录...</p> | |
| </div> | |
| ) : userOrders.length === 0 ? ( | |
| <div className="py-20 text-center opacity-20"> | |
| <Receipt size={48} className="mx-auto mb-4" /> | |
| <p className="text-sm font-bold">暂无支付记录</p> | |
| </div> | |
| ) : ( | |
| <div className="space-y-3"> | |
| {userOrders.map((order) => ( | |
| <div key={order.order_id} className="bg-white/[0.02] border border-white/5 rounded-2xl p-4 flex flex-col gap-3"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex flex-col gap-0.5"> | |
| <span className="text-[10px] text-gray-500 font-bold uppercase tracking-wider">订单号</span> | |
| <span className="text-xs font-mono text-gray-300 select-all">{order.order_id}</span> | |
| </div> | |
| <button | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| handleToggleOrderStatus(order.order_id, order.status); | |
| }} | |
| className={`px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-tighter border transition-all active:scale-95 ${order.status === 'paid' | |
| ? 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20 hover:bg-emerald-500/20' | |
| : order.status === 'pending' | |
| ? 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20 hover:bg-yellow-500/20' | |
| : 'bg-white/5 text-gray-500 border-white/10' | |
| }`} | |
| > | |
| {order.status === 'paid' ? '已支付' : order.status === 'pending' ? '待支付' : order.status} | |
| </button> | |
| </div> | |
| <div className="grid grid-cols-3 gap-4 border-t border-white/5 pt-3"> | |
| <div className="flex flex-col gap-0.5"> | |
| <span className="text-[10px] text-gray-500 font-bold uppercase tracking-wider">金额 / 时长</span> | |
| <div className="flex items-center gap-1.5"> | |
| <span className="text-gray-200 font-bold text-sm">¥{order.amount}</span> | |
| <span className="text-[10px] text-gray-500">({order.months}个月)</span> | |
| </div> | |
| </div> | |
| <div className="flex flex-col gap-0.5"> | |
| <span className="text-[10px] text-gray-500 font-bold uppercase tracking-wider">方式</span> | |
| <span className="text-gray-300 text-xs font-bold">{order.pay_type === 1 ? '支付宝' : '微信'}</span> | |
| </div> | |
| <div className="flex flex-col gap-0.5"> | |
| <span className="text-[10px] text-gray-500 font-bold uppercase tracking-wider">最后更新</span> | |
| <span className="text-gray-300 text-[10px] font-bold font-mono">{order.paid_at || order.created_at}</span> | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |