import { useEffect, useState, useCallback } from 'react'; import ReactEChartsCore from 'echarts-for-react/lib/core'; import * as echarts from 'echarts/core'; import { LineChart, BarChart } from 'echarts/charts'; import { GridComponent, TooltipComponent, LegendComponent, DataZoomComponent } from 'echarts/components'; import { CanvasRenderer } from 'echarts/renderers'; import { teamApi } from '../lib/api'; import type { TeamInfo, TeamStats } from '../types'; import { showToast } from '../components/Toast'; import styles from './DashboardPage.module.css'; echarts.use([LineChart, BarChart, GridComponent, TooltipComponent, LegendComponent, DataZoomComponent, CanvasRenderer]); export function TeamDashboardPage() { const [info, setInfo] = useState<(TeamInfo & { daily_member_limit_default: number; member_count: number }) | null>(null); const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const fetchData = useCallback(async () => { try { const [infoRes, statsRes] = await Promise.all([ teamApi.getInfo(), teamApi.getStats(), ]); setInfo(infoRes.data); setStats(statsRes.data); } catch { showToast('加载团队数据失败'); } finally { setLoading(false); } }, []); useEffect(() => { fetchData(); }, [fetchData]); if (loading) { return (
{[1, 2, 3, 4, 5].map((i) =>
)}
); } if (!info || !stats) return null; const fmtMoney = (val: number) => '¥' + (val || 0).toFixed(2); const statCards = [ { label: '余额', value: fmtMoney(info.balance) }, { label: '累计消费', value: fmtMoney(info.total_spent) }, { label: '可用余额', value: fmtMoney(info.available_balance) }, { label: '月消费限额', value: fmtMoney(info.monthly_spending_limit) }, { label: '本月消费', value: fmtMoney(info.monthly_spent) }, ]; const trendOption: echarts.EChartsCoreOption = { tooltip: { trigger: 'axis', backgroundColor: 'rgba(13, 13, 26, 0.95)', borderColor: 'rgba(255, 255, 255, 0.10)', textStyle: { color: '#f1f0ff', fontSize: 12 }, formatter: (params: unknown) => { const p = (params as { name: string; value: number }[])[0]; return `${p.name}
消费: ¥${p.value.toFixed(2)}`; }, }, grid: { left: 50, right: 20, top: 20, bottom: 60 }, xAxis: { type: 'category', data: stats.daily_trend.map((d) => d.date.slice(5)), axisLabel: { color: '#8b8ea8', fontSize: 11 }, axisLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.08)' } }, }, yAxis: { type: 'value', axisLabel: { color: '#8b8ea8', fontSize: 11 }, splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.06)' } }, }, dataZoom: [{ type: 'inside', start: 0, end: 100 }], series: [{ type: 'line', data: stats.daily_trend.map((d) => d.cost ?? d.seconds), smooth: true, lineStyle: { color: '#6c63ff', width: 2 }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: 'rgba(108, 99, 255, 0.25)' }, { offset: 1, color: 'rgba(108, 99, 255, 0.02)' }, ]), }, itemStyle: { color: '#6c63ff' }, }], }; const sortedMembers = [...stats.member_consumption].sort((a, b) => (a.cost_consumed ?? a.seconds_consumed) - (b.cost_consumed ?? b.seconds_consumed)); const barOption: echarts.EChartsCoreOption = { tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, backgroundColor: 'rgba(13, 13, 26, 0.95)', borderColor: 'rgba(255, 255, 255, 0.10)', textStyle: { color: '#f1f0ff', fontSize: 12 }, }, grid: { left: 80, right: 40, top: 10, bottom: 20 }, xAxis: { type: 'value', axisLabel: { color: '#8b8ea8', fontSize: 11 }, splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.06)' } }, }, yAxis: { type: 'category', data: sortedMembers.map((m) => m.username), axisLabel: { color: '#8b8ea8', fontSize: 12 }, axisLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.08)' } }, }, series: [{ type: 'bar', data: sortedMembers.map((m) => m.cost_consumed ?? m.seconds_consumed), barWidth: 16, itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [ { offset: 0, color: '#6c63ff' }, { offset: 1, color: '#8b5cf6' }, ]), borderRadius: [0, 4, 4, 0], }, label: { show: true, position: 'right', color: '#8b8ea8', fontSize: 11, formatter: (p: { value: number }) => `¥${p.value.toFixed(2)}`, }, }], }; return (

团队概览

{statCards.map((card) => (
{card.label}
{card.value}
))}

团队消费趋势(近30天 · 元)

成员消费排行

); }