89fd333702
Add client-logging SDK, expanded event taxonomy, API observability, admin domain events UI, and move SENTRY_DSN to HQ system settings with @sentry/node bootstrap after config preload. Co-authored-by: Cursor <cursoragent@cursor.com>
137 lines
5.2 KiB
TypeScript
137 lines
5.2 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react';
|
|
import { useNavigate, Link } from 'react-router-dom';
|
|
import type { PartnerLeaderboardPeriod, PartnerLeaderboardResponse } from '@dukang/shared-types';
|
|
import PageHeader from '@dukang/shared-ui/PageHeader';
|
|
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
|
|
import { fetchPartnerLeaderboard } from '../lib/leaderboard';
|
|
import { usePartnerSession } from '../contexts/PartnerSessionContext';
|
|
import { isPrimaryAccount } from '../lib/partnerAccess';
|
|
import { usePartnerPageView } from '../lib/usePageView';
|
|
|
|
const PERIOD_TABS: { key: PartnerLeaderboardPeriod; label: string }[] = [
|
|
{ key: 'month', label: '本月' },
|
|
{ key: 'lastMonth', label: '上月' },
|
|
{ key: 'total', label: '累计' },
|
|
];
|
|
|
|
function periodStatLabel(period: PartnerLeaderboardPeriod): string {
|
|
if (period === 'month') return '本月拓店';
|
|
if (period === 'lastMonth') return '上月拓店';
|
|
return '累计拓店';
|
|
}
|
|
|
|
function periodSubLabel(period: PartnerLeaderboardPeriod): string {
|
|
if (period === 'total') return '累计';
|
|
if (period === 'month') return '本月新增';
|
|
return '上月新增';
|
|
}
|
|
|
|
export default function LeaderboardPage() {
|
|
usePartnerPageView('partner_leaderboard_view');
|
|
const navigate = useNavigate();
|
|
const { account } = usePartnerSession();
|
|
const showStaffFab = isPrimaryAccount(account);
|
|
const [period, setPeriod] = useState<PartnerLeaderboardPeriod>('month');
|
|
const [data, setData] = useState<PartnerLeaderboardResponse | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState('');
|
|
|
|
const loadLeaderboard = useCallback(() => {
|
|
setLoading(true);
|
|
setError('');
|
|
return fetchPartnerLeaderboard(period)
|
|
.then(setData)
|
|
.catch((e) => {
|
|
setData(null);
|
|
setError(e instanceof Error ? e.message : '加载失败');
|
|
})
|
|
.finally(() => setLoading(false));
|
|
}, [period]);
|
|
|
|
useEffect(() => {
|
|
void loadLeaderboard();
|
|
}, [loadLeaderboard]);
|
|
|
|
return (
|
|
<PullToRefresh onRefresh={loadLeaderboard} className="page-no-tab">
|
|
<PageHeader title="团队贡献榜" onBack={() => navigate('/')} />
|
|
|
|
<div className="partner-leaderboard-tabs">
|
|
{PERIOD_TABS.map((tab) => (
|
|
<button
|
|
key={tab.key}
|
|
type="button"
|
|
className={`partner-leaderboard-tab${period === tab.key ? ' active' : ''}`}
|
|
onClick={() => setPeriod(tab.key)}
|
|
>
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<p className="label-md text-muted" style={{ padding: '0 20px 12px' }}>
|
|
按拓店数排行,数据实时更新
|
|
</p>
|
|
|
|
{loading && <div className="empty">加载中...</div>}
|
|
{!loading && error && <div className="empty">{error}</div>}
|
|
|
|
{!loading && !error && data && (
|
|
<div style={{ padding: '0 20px 96px' }}>
|
|
{data.self && (
|
|
<section className="partner-leaderboard-self-card">
|
|
<div className="partner-leaderboard-self-main">
|
|
<div className="partner-leaderboard-self-rank">第 {data.self.rank} 名</div>
|
|
<div>
|
|
<p className="headline-md" style={{ color: '#fff' }}>
|
|
{data.self.name}
|
|
{data.self.isSelf ? ' (我)' : ''}
|
|
</p>
|
|
<p className="label-md" style={{ color: 'rgba(255,255,255,0.8)' }}>{data.self.roleLabel}</p>
|
|
</div>
|
|
</div>
|
|
<div className="partner-leaderboard-self-stats">
|
|
<span>{periodStatLabel(period)}: {data.self.periodStores} 间</span>
|
|
{data.self.beatPercent != null && (
|
|
<span>击败 {data.self.beatPercent}% 合伙人</span>
|
|
)}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
<h2 className="headline-md" style={{ margin: '16px 0 12px' }}>实时排行</h2>
|
|
|
|
{data.list.length === 0 ? (
|
|
<div className="empty">暂无排行数据</div>
|
|
) : (
|
|
data.list.map((entry) => (
|
|
<div key={entry.accountId} className="partner-leaderboard-row">
|
|
<div className="partner-leaderboard-rank">{entry.rank}</div>
|
|
<div className="partner-leaderboard-info">
|
|
<p className="headline-md">
|
|
{entry.name}
|
|
<span className="label-md text-muted" style={{ marginLeft: 6, fontWeight: 400 }}>
|
|
({entry.roleLabel})
|
|
</span>
|
|
</p>
|
|
<p className="label-md text-muted">累计拓店 {entry.totalStores} 间</p>
|
|
</div>
|
|
<div className="partner-leaderboard-stat">
|
|
<p className="headline-md text-primary">{entry.periodStores} 间</p>
|
|
<p className="label-md text-muted">{periodSubLabel(period)}</p>
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{showStaffFab && (
|
|
<Link to="/center/staff" className="partner-fab-round" aria-label="子账号管理">
|
|
<span className="material-symbols-outlined">add</span>
|
|
</Link>
|
|
)}
|
|
</PullToRefresh>
|
|
);
|
|
}
|