import { useState, useEffect, useCallback } from 'react'; import { listLinks, updateLink, deleteLink, type ClientRule, type IpRule, type LinkItem, type GeoRule } from '../api'; import EditDrawer from './EditDrawer'; const PAGE_SIZE = 20; function formatDate(ts: number | null): string { if (!ts) return '—'; return new Date(ts * 1000).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }); } function geoBadge(item: LinkItem): JSX.Element | null { if (!item.geo_rule) return null; let rule: GeoRule; try { rule = JSON.parse(item.geo_rule); } catch { return null; } const label = rule.mode === 'allow' ? `仅 ${rule.countries.join('/')}` : `屏蔽 ${rule.countries.join('/')}`; return {label}; } function clientBadge(item: LinkItem): JSX.Element | null { if (!item.client_rule) return null; let r: ClientRule; try { r = JSON.parse(item.client_rule); } catch { return null; } const parts: string[] = []; if (r.device === 'mobile') parts.push('仅手机'); if (r.device === 'desktop') parts.push('仅电脑'); if (r.browsers && r.browsers.length) { const m: Record = { chrome: 'Chrome', edge: 'Edge', safari: 'Safari', firefox: 'Fx', opera: 'Op', other: '其他' }; parts.push(r.browsers.map(b => m[b] || b).join('+')); } if (!parts.length) return null; const label = [r.device && (r.device === 'mobile' ? '手机' : '电脑'), r.browsers?.length ? `浏览器: ${r.browsers.join(',')}` : ''].filter(Boolean).join(' · '); return ( {parts.join(' · ')} ); } function ipBadge(item: LinkItem): JSX.Element | null { if (!item.ip_rule) return null; let r: IpRule; try { r = JSON.parse(item.ip_rule); } catch { return null; } const n = r.ips?.length ?? 0; if (!n) return null; const short = r.mode === 'allow' ? `白${n}条` : `黑${n}条`; return ( {short} ); } function expiredBadge(item: LinkItem): JSX.Element { const now = Math.floor(Date.now() / 1000); if (item.expires_at && now > item.expires_at) { return 已过期; } if (item.max_clicks !== null && item.clicks >= item.max_clicks) { return 已满; } if (item.deleted) return 停用; return 有效; } export default function AdminPanel() { const [items, setItems] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [editing, setEditing] = useState(null); const [confirmDelete, setConfirmDelete] = useState(null); const load = useCallback(async (p: number) => { setLoading(true); setError(''); try { const data = await listLinks(p, PAGE_SIZE); setItems(data.items); setTotal(data.total); } catch (e) { setError(String(e)); } finally { setLoading(false); } }, []); useEffect(() => { load(page); }, [page, load]); const handleDelete = async (code: string) => { try { await deleteLink(code); setConfirmDelete(null); load(page); } catch (e) { setError(String(e)); } }; const handleToggleDeleted = async (item: LinkItem) => { try { await updateLink(item.code, { deleted: item.deleted ? 0 : 1 }); load(page); } catch (e) { setError(String(e)); } }; const handleSave = async () => { setEditing(null); load(page); }; const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); return (
{total}
{error &&
{error}
} {loading &&

加载中

} {editing && ( setEditing(null)} /> )}
{items.length === 0 && !loading && ( )} {items.map(item => ( ))}
短码 目标链接 状态 点击 过期时间 创建时间 操作
暂无数据
{item.code} {item.has_password ? ( ) : null} {geoBadge(item)} {clientBadge(item)} {ipBadge(item)} {item.target_url.length > 45 ? item.target_url.slice(0, 45) + '…' : item.target_url} {expiredBadge(item)} {item.clicks} {item.max_clicks !== null && ( / {item.max_clicks} )} {formatDate(item.expires_at)} {formatDate(item.created_at)}
{confirmDelete === item.code ? ( <> ) : ( )}
{totalPages > 1 && (
{Array.from({ length: totalPages }, (_, i) => i + 1) .filter(p => Math.abs(p - page) <= 2 || p === 1 || p === totalPages) .reduce<(number | '...')[]>((acc, p, idx, arr) => { if (idx > 0 && (arr[idx - 1] as number) < p - 1) acc.push('...'); acc.push(p); return acc; }, []) .map((p, i) => p === '...' ? : ( ) ) }
)}
); }