初始提交:全国记者站管理系统

This commit is contained in:
selfrelease
2026-08-01 23:09:49 +08:00
commit 45fbba0308
96 changed files with 21514 additions and 0 deletions
+188
View File
@@ -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>
</>
)}
</>
)
}