| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276 |
- 'use client';
- import { useState, useEffect } from 'react';
- import { Plus, Search, RefreshCw, X, Lock } from 'lucide-react';
- import api from '@/lib/api';
- import { toast } from 'react-hot-toast';
- // 引入拆分后的组件
- import AccountTable, { Account } from '@/components/admin/accounts/AccountTable';
- import Pagination from '@/components/common/Pagination';
- export default function AdminAccountsPage() {
- // === 1. 状态定义 ===
- const [loading, setLoading] = useState<boolean>(true);
- const [data, setData] = useState<Account[]>([]);
-
- // 分页与搜索
- const [page, setPage] = useState(1); // 1-based UI
- const [pageSize] = useState(10);
- const [total, setTotal] = useState(0);
- const [keyword, setKeyword] = useState('');
- // 模态框状态
- const [showAddModal, setShowAddModal] = useState(false);
- const [showLockModal, setShowLockModal] = useState(false);
- const [selectedAccount, setSelectedAccount] = useState<Account | null>(null);
- // 表单状态
- const [addForm, setAddForm] = useState({
- pool_name: '',
- username: '',
- password: '',
- extra_data_str: '{}'
- });
- const [lockDuration, setLockDuration] = useState<number>(3600);
- // === 2. 数据获取 ===
- const fetchAccounts = async (targetPage: number = page) => {
- setLoading(true);
- try {
- // API 使用 0-based,前端使用 1-based
- // const apiPage = targetPage - 1;
- const res = await api.get('/api/account/list_all', {
- params: {
- page: targetPage,
- size: pageSize,
- keyword: keyword
- }
- });
- const resData = res.data?.data || {};
- if (resData && Array.isArray(resData.items)) {
- setData(resData.items);
- setTotal(resData.total || 0);
- } else {
- setData([]);
- setTotal(0);
- }
-
- setPage(targetPage);
- // 翻页滚动优化
- if (targetPage > 1) {
- window.scrollTo({ top: 0, behavior: 'smooth' });
- }
- } catch (error) {
- console.error("Fetch accounts failed", error);
- toast.error('获取列表失败');
- setData([]);
- } finally {
- setLoading(false);
- }
- };
- useEffect(() => {
- fetchAccounts(1);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
- const handleSearch = () => fetchAccounts(1);
- const handleKeyDown = (e: React.KeyboardEvent) => {
- if (e.key === 'Enter') handleSearch();
- };
- // === 3. 业务逻辑 ===
- const handleAddAccount = async () => {
- try {
- let extraDataJson = {};
- try {
- extraDataJson = JSON.parse(addForm.extra_data_str);
- } catch (e) {
- toast.error('Extra Data 必须是 JSON 格式');
- return;
- }
- await api.post('/api/account/add', {
- pool_name: addForm.pool_name,
- username: addForm.username,
- password: addForm.password,
- extra_data: extraDataJson
- });
- toast.success('添加成功');
- setShowAddModal(false);
- setAddForm({ pool_name: '', username: '', password: '', extra_data_str: '{}' });
- fetchAccounts(1);
- } catch (error) {
- toast.error('添加失败');
- }
- };
- const handleLockConfirm = async () => {
- if (!selectedAccount) return;
- try {
- await api.post('/api/account/lock', {
- pool_name: selectedAccount.pool_name,
- username: selectedAccount.username,
- duration: Number(lockDuration)
- });
- toast.success('锁定成功');
- setShowLockModal(false);
- fetchAccounts(page); // 刷新当前页
- } catch (error) {
- toast.error('锁定失败');
- }
- };
- const handleDisable = async (account: Account) => {
- if (!confirm(`确定要禁用账号 ${account.username} 吗?`)) return;
- try {
- await api.post('/api/account/disable', {
- pool_name: account.pool_name,
- username: account.username
- });
- toast.success('禁用成功');
- fetchAccounts(page);
- } catch (error) {
- toast.error('禁用失败');
- }
- };
- return (
- <div className="p-4 md:p-6 space-y-6">
-
- {/* === 头部区域 === */}
- <div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
- <div>
- <h1 className="text-2xl font-bold text-slate-800">账号池管理</h1>
- <p className="text-sm text-slate-500 mt-1">管理爬虫账号、查看锁定状态及禁用异常账号</p>
- </div>
-
- <div className="flex flex-col sm:flex-row gap-3 w-full md:w-auto">
- {/* 搜索框 */}
- <div className="relative w-full sm:w-auto md:w-64">
- <input
- type="text"
- placeholder="搜索账号 / Pool..."
- className="w-full pl-9 pr-4 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none transition shadow-sm"
- value={keyword}
- onChange={e => setKeyword(e.target.value)}
- onKeyDown={handleKeyDown}
- />
- <Search size={16} className="absolute left-3 top-2.5 text-gray-400" />
- </div>
- {/* 刷新 */}
- <button
- onClick={() => fetchAccounts(page)}
- className="flex items-center justify-center gap-2 px-4 py-2 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 text-slate-600 font-medium transition shadow-sm w-full sm:w-auto"
- title="刷新列表"
- >
- <RefreshCw size={16} />
- <span className="sm:hidden">刷新</span>
- </button>
- {/* 新增 */}
- <button
- onClick={() => setShowAddModal(true)}
- className="flex items-center justify-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-lg text-sm font-medium hover:bg-slate-800 transition shadow-sm w-full sm:w-auto"
- >
- <Plus size={16} /> 新增
- </button>
- </div>
- </div>
- {/* === 表格组件 === */}
- <AccountTable
- data={data}
- loading={loading}
- onLock={(acc) => { setSelectedAccount(acc); setShowLockModal(true); }}
- onDisable={handleDisable}
- />
- {/* === 分页组件 === */}
- <div className="mt-4">
- <Pagination
- currentPage={page}
- total={total}
- pageSize={pageSize}
- onPageChange={fetchAccounts} // 传递函数,分页组件点击时回调
- />
- </div>
- {/* === Modals === */}
- {/* 新增 Modal */}
- {showAddModal && (
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in zoom-in-95 duration-200">
- <div className="bg-white rounded-xl shadow-2xl w-full max-w-md overflow-hidden">
- <div className="px-6 py-4 border-b border-slate-100 flex justify-between items-center bg-slate-50/50">
- <h3 className="font-bold text-lg text-slate-800">新增账号</h3>
- <button onClick={() => setShowAddModal(false)} className="text-slate-400 hover:text-slate-600">
- <X size={20} />
- </button>
- </div>
- <div className="p-6 space-y-4 max-h-[80vh] overflow-y-auto">
- <div>
- <label className="block text-xs font-bold uppercase text-slate-500 mb-1.5">Pool Name</label>
- <input type="text" className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none text-sm" value={addForm.pool_name} onChange={e => setAddForm({...addForm, pool_name: e.target.value})} />
- </div>
- <div>
- <label className="block text-xs font-bold uppercase text-slate-500 mb-1.5">Username</label>
- <input type="text" className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none text-sm" value={addForm.username} onChange={e => setAddForm({...addForm, username: e.target.value})} />
- </div>
- <div>
- <label className="block text-xs font-bold uppercase text-slate-500 mb-1.5">Password</label>
- <input type="text" className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none text-sm" value={addForm.password} onChange={e => setAddForm({...addForm, password: e.target.value})} />
- </div>
- <div>
- <label className="block text-xs font-bold uppercase text-slate-500 mb-1.5">Extra Data (JSON)</label>
- <textarea className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none font-mono text-xs bg-slate-50" rows={4} value={addForm.extra_data_str} onChange={e => setAddForm({...addForm, extra_data_str: e.target.value})} placeholder='{"country": "fr"}' />
- </div>
- </div>
- <div className="px-6 py-4 bg-slate-50 flex justify-end gap-3 border-t">
- <button onClick={() => setShowAddModal(false)} className="px-4 py-2 text-sm text-slate-600 hover:bg-slate-200 rounded-lg">取消</button>
- <button onClick={handleAddAccount} className="px-4 py-2 text-sm font-bold bg-slate-900 text-white rounded-lg hover:bg-slate-800">确认添加</button>
- </div>
- </div>
- </div>
- )}
- {/* 锁定 Modal */}
- {showLockModal && selectedAccount && (
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in zoom-in-95 duration-200">
- <div className="bg-white rounded-xl shadow-2xl w-full max-w-sm overflow-hidden">
- <div className="px-6 py-4 border-b border-slate-100 flex justify-between items-center bg-orange-50/50">
- <h3 className="font-bold text-lg text-orange-900 flex items-center gap-2">
- <Lock size={18} /> 锁定账号
- </h3>
- <button onClick={() => setShowLockModal(false)} className="text-slate-400 hover:text-slate-600">
- <X size={20} />
- </button>
- </div>
- <div className="p-6 space-y-4">
- <p className="text-sm text-slate-600 bg-orange-50 p-3 rounded-lg border border-orange-100">
- 正在锁定: <span className="font-bold text-slate-900">{selectedAccount.username}</span>
- </p>
- <div>
- <label className="block text-xs font-bold uppercase text-slate-500 mb-1.5">持续时间 (秒)</label>
- <input type="number" min="0" className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-orange-500 outline-none font-mono" value={lockDuration} onChange={e => setLockDuration(Number(e.target.value))} />
- <div className="flex gap-2 mt-2">
- <button onClick={() => setLockDuration(3600)} className="flex-1 text-xs bg-slate-100 border px-2 py-1 rounded hover:bg-slate-200">1小时</button>
- <button onClick={() => setLockDuration(86400)} className="flex-1 text-xs bg-slate-100 border px-2 py-1 rounded hover:bg-slate-200">1天</button>
- </div>
- </div>
- </div>
- <div className="px-6 py-4 bg-slate-50 flex justify-end gap-3 border-t">
- <button onClick={() => setShowLockModal(false)} className="px-4 py-2 text-sm text-slate-600 hover:bg-slate-200 rounded-lg">取消</button>
- <button onClick={handleLockConfirm} className="px-4 py-2 text-sm font-bold bg-orange-600 text-white rounded-lg hover:bg-orange-700">确认锁定</button>
- </div>
- </div>
- </div>
- )}
- </div>
- );
- }
|