Files
ReporterStationManagementSy…/src/pages/Leaderboard/index.tsx
T
2026-08-01 23:09:49 +08:00

188 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState, useCallback } from 'react'
import { Trophy, Medal, Award, BarChart3, Users } from 'lucide-react'
import { useRole } from '../../context'
import { api, type LeaderboardResponse, type ReporterRank, type StationRank } from '../../api'
const PERIODS = ['2026-Q1', '2026-Q2', '2026-Q3', '2026-Q4']
function RankBadge({ rank }: { rank: number }) {
if (rank === 1) return <div className="lb-badge lb-gold"><Trophy size={13} /></div>
if (rank === 2) return <div className="lb-badge lb-silver"><Medal size={13} /></div>
if (rank === 3) return <div className="lb-badge lb-bronze"><Award size={13} /></div>
return <div className="lb-badge">{rank}</div>
}
function ReporterRow({ item, isMe }: { item: ReporterRank; isMe: boolean }) {
return (
<tr className={isMe ? 'lb-me' : ''}>
<td data-label="排名"><RankBadge rank={item.rank} /></td>
<td data-label="记者">
<div className="lb-name">
{isMe && <span className="lb-me-tag"></span>}
{item.name}
</div>
</td>
<td data-label="记者站" className="lb-muted">{item.station}</td>
<td data-label="综合得分" className="lb-score">{(item.totalScore ?? 0).toFixed(1)}</td>
</tr>
)
}
function StationRow({ item, isMe }: { item: StationRank; isMe: boolean }) {
return (
<tr className={isMe ? 'lb-me' : ''}>
<td data-label="排名"><RankBadge rank={item.rank} /></td>
<td data-label="记者站">
<div className="lb-name">
{isMe && <span className="lb-me-tag"></span>}
{item.name}
</div>
</td>
<td data-label="人数" className="lb-muted">{item.reporterCount} </td>
<td data-label="质量" className="lb-dim">{(item.avgQuality ?? 0).toFixed(1)}</td>
<td data-label="数量" className="lb-dim">{(item.avgQuantity ?? 0).toFixed(1)}</td>
<td data-label="时效" className="lb-dim">{(item.avgEfficiency ?? 0).toFixed(1)}</td>
<td data-label="综合得分" className="lb-score">{(item.avgScore ?? 0).toFixed(1)}</td>
</tr>
)
}
export function LeaderboardPage() {
const { role } = useRole()
const [data, setData] = useState<LeaderboardResponse | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [period, setPeriod] = useState('2026-Q3')
const [groupBy, setGroupBy] = useState<'reporter' | 'station'>('reporter')
const load = useCallback(async () => {
setLoading(true); setError('')
try {
const result = await api.leaderboard(role, { period, group_by: groupBy })
setData(result)
} catch (e: any) { setError(e.message) }
finally { setLoading(false) }
}, [role, period, groupBy])
useEffect(() => { load() }, [load])
const myRank = data?.myRank
const ranks = data?.ranks ?? []
return (
<>
<div className="page-heading small">
<div>
<div className="page-heading-icon"><Trophy size={20} /></div>
<div>
<h1></h1>
<span>
{groupBy === 'reporter'
? '按考核周期统计全国记者积分排名。'
: '按考核周期统计各记者站平均积分排名。'}
</span>
</div>
</div>
</div>
{/* 筛选栏 */}
<div className="panel" style={{ marginBottom: 16 }}>
<div className="filters">
<select value={period} onChange={e => setPeriod(e.target.value)}>
{PERIODS.map(p => <option key={p} value={p}>{p}</option>)}
</select>
<div className="lb-toggle">
<button
className={groupBy === 'reporter' ? 'active' : ''}
onClick={() => setGroupBy('reporter')}
>
<BarChart3 size={13} />
</button>
<button
className={groupBy === 'station' ? 'active' : ''}
onClick={() => setGroupBy('station')}
>
<Users size={13} />
</button>
</div>
<span className="result-count">
{loading ? '加载中...' : `${ranks.length} 条记录`}
{myRank != null && role !== 'headquarters' && ` · 我的排名:第 ${myRank}`}
</span>
</div>
</div>
{error && (
<div className="error-banner">{error}</div>
)}
{!loading && !error && data && ranks.length === 0 && (
<div className="empty panel" style={{ padding: '40px 0' }}>
</div>
)}
{!loading && !error && ranks.length > 0 && (
<>
{/* 我的排名提示 */}
{data != null && myRank != null && role !== 'headquarters' && (
<div className="lb-mine">
<span className="lb-mine-label"></span>
<strong className="lb-mine-rank"> {myRank} </strong>
<span className="lb-mine-total">/ {data.ranks.length} </span>
</div>
)}
<div className="panel lb-table-wrap">
{groupBy === 'reporter' ? (
<table className="lb-table">
<thead>
<tr>
<th style={{ width: 56 }}></th>
<th></th>
<th></th>
<th style={{ width: 90 }}></th>
</tr>
</thead>
<tbody>
{(ranks as ReporterRank[]).map(item => (
<ReporterRow
key={item.name}
item={item}
isMe={item.rank === myRank}
/>
))}
</tbody>
</table>
) : (
<table className="lb-table">
<thead>
<tr>
<th style={{ width: 56 }}></th>
<th></th>
<th style={{ width: 70 }}></th>
<th style={{ width: 64 }}></th>
<th style={{ width: 64 }}></th>
<th style={{ width: 64 }}></th>
<th style={{ width: 90 }}></th>
</tr>
</thead>
<tbody>
{(ranks as StationRank[]).map(item => (
<StationRow
key={item.name}
item={item}
isMe={item.rank === myRank}
/>
))}
</tbody>
</table>
)}
</div>
</>
)}
</>
)
}