Paper_Trading / frontend /src /components /AdminPanel.tsx
superxu520's picture
fix: 修复日期格式转换问题
4d2e3e5
'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>
);
}