tangweijie e0a6ba2dc4 feat: 初始化 React 数据看板项目
- Vite + React 18 + TypeScript
- TailwindCSS 暗色主题
- 仪表板、分析、事件浏览页面
- Recharts 图表组件
- Zustand 状态管理
- TanStack Query 数据请求
2026-01-05 18:08:27 +08:00

121 lines
5.1 KiB
TypeScript

import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { api } from '@/services/api';
import { cn } from '@/lib/utils';
import { Search, Filter, ChevronLeft, ChevronRight } from 'lucide-react';
export default function Events() {
const [page, setPage] = useState(1);
const [search, setSearch] = useState('');
const limit = 20;
const { data: events, isLoading } = useQuery({
queryKey: ['events', page, limit],
queryFn: () => api.getEvents({ skip: (page - 1) * limit, limit }),
});
return (
<div className="space-y-6 animate-fade-in">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-display font-bold"></h1>
<p className="text-zinc-500 text-sm mt-1"></p>
</div>
</div>
{/* Filters */}
<div className="flex items-center gap-4">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-500" />
<input
type="text"
placeholder="搜索事件..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full pl-10 pr-4 py-2 bg-[var(--card)] border border-[var(--border)] rounded-lg text-sm placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-primary-500/50"
/>
</div>
<button className="flex items-center gap-2 px-4 py-2 bg-[var(--card)] border border-[var(--border)] rounded-lg text-sm hover:bg-zinc-800 transition-colors">
<Filter className="w-4 h-4" />
</button>
</div>
{/* Events Table */}
<div className="bg-[var(--card)] rounded-xl border border-[var(--border)] overflow-hidden">
<table className="w-full">
<thead className="bg-zinc-800/50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-zinc-400 uppercase">ID</th>
<th className="px-6 py-3 text-left text-xs font-medium text-zinc-400 uppercase"></th>
<th className="px-6 py-3 text-left text-xs font-medium text-zinc-400 uppercase"></th>
<th className="px-6 py-3 text-left text-xs font-medium text-zinc-400 uppercase">IDE</th>
<th className="px-6 py-3 text-left text-xs font-medium text-zinc-400 uppercase"></th>
</tr>
</thead>
<tbody className="divide-y divide-[var(--border)]">
{isLoading ? (
[...Array(10)].map((_, i) => (
<tr key={i}>
<td colSpan={5} className="px-6 py-4">
<div className="h-4 bg-zinc-800 rounded animate-pulse" />
</td>
</tr>
))
) : (
(events ?? []).map((event) => (
<tr key={event.eventId} className="hover:bg-zinc-800/30 transition-colors">
<td className="px-6 py-4 text-sm font-mono text-zinc-300">
{event.eventId.substring(0, 8)}...
</td>
<td className="px-6 py-4">
<span className={cn(
'px-2 py-1 text-xs rounded-full',
event.eventType.includes('accepted')
? 'bg-emerald-500/10 text-emerald-400'
: event.eventType.includes('rejected')
? 'bg-rose-500/10 text-rose-400'
: 'bg-zinc-500/10 text-zinc-400'
)}>
{event.eventType.replace(/_/g, ' ')}
</span>
</td>
<td className="px-6 py-4 text-sm text-zinc-300">{event.language ?? '-'}</td>
<td className="px-6 py-4 text-sm text-zinc-300">{event.ideType}</td>
<td className="px-6 py-4 text-sm text-zinc-500">
{new Date(event.timestamp).toLocaleString()}
</td>
</tr>
))
)}
</tbody>
</table>
{/* Pagination */}
<div className="flex items-center justify-between px-6 py-4 border-t border-[var(--border)]">
<p className="text-sm text-zinc-500">
{(page - 1) * limit + 1} - {page * limit}
</p>
<div className="flex items-center gap-2">
<button
onClick={() => setPage(Math.max(1, page - 1))}
disabled={page === 1}
className="p-2 rounded-lg border border-[var(--border)] disabled:opacity-50 hover:bg-zinc-800 transition-colors"
>
<ChevronLeft className="w-4 h-4" />
</button>
<span className="text-sm text-zinc-400"> {page} </span>
<button
onClick={() => setPage(page + 1)}
className="p-2 rounded-lg border border-[var(--border)] hover:bg-zinc-800 transition-colors"
>
<ChevronRight className="w-4 h-4" />
</button>
</div>
</div>
</div>
</div>
);
}