初始提交:全国记者站管理系统
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user