Skip to content

Commit a8088db

Browse files
committed
perf: ⚡ 重构统计图和排行榜组件,迁移状态管理到home视图存储
1 parent 1c89754 commit a8088db

File tree

4 files changed

+72
-73
lines changed

4 files changed

+72
-73
lines changed

web/src/components/modules/home/chart.tsx

Lines changed: 28 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,19 @@ import { useTranslations } from 'next-intl';
88
import { formatCount, formatMoney } from '@/lib/utils';
99
import dayjs from 'dayjs';
1010
import { AnimatedNumber } from '@/components/common/AnimatedNumber';
11-
import { Tabs, TabsList, TabsTrigger, TabsContents, TabsContent } from '@/components/animate-ui/components/animate/tabs';
12-
import { useToolbarViewOptionsStore, type ChartMetricType } from '@/components/modules/toolbar/view-options-store';
13-
14-
const PERIODS = ['1', '7', '30'] as const;
15-
const METRIC_TYPES: ChartMetricType[] = ['cost', 'count', 'tokens'];
11+
import { Tabs, TabsList, TabsTrigger } from '@/components/animate-ui/components/animate/tabs';
12+
import { useHomeViewStore, type ChartMetricType, type ChartPeriod } from '@/components/modules/home/store';
1613

1714
export function StatsChart() {
15+
const PERIODS: readonly ChartPeriod[] = ['1', '7', '30'];
1816
const { data: statsDaily } = useStatsDaily();
1917
const { data: statsHourly } = useStatsHourly();
2018
const t = useTranslations('home.chart');
21-
22-
const chartMetricType = useToolbarViewOptionsStore((state) => state.chartMetricType);
23-
const setChartMetricType = useToolbarViewOptionsStore((state) => state.setChartMetricType);
24-
const period = useToolbarViewOptionsStore((state) => {
25-
if (!state.chartPeriod) return '1';
26-
return state.chartPeriod;
27-
});
28-
const setChartPeriod = useToolbarViewOptionsStore((state) => state.setChartPeriod);
19+
20+
const chartMetricType = useHomeViewStore((state) => state.chartMetricType);
21+
const setChartMetricType = useHomeViewStore((state) => state.setChartMetricType);
22+
const period = useHomeViewStore((state) => state.chartPeriod);
23+
const setChartPeriod = useHomeViewStore((state) => state.setChartPeriod);
2924

3025
const sortedDaily = useMemo(() => {
3126
if (!statsDaily) return [];
@@ -42,51 +37,49 @@ export function StatsChart() {
4237
if (!statsHourly) return [];
4338
return statsHourly.map((stat) => ({
4439
date: `${stat.hour}:00`,
45-
[dataKey]: chartMetricType === 'cost'
46-
? stat.total_cost.raw
40+
[dataKey]: chartMetricType === 'cost'
41+
? stat.total_cost.raw
4742
: chartMetricType === 'count'
48-
? stat.request_count.raw
49-
: (stat.input_token.raw + stat.output_token.raw),
43+
? stat.request_count.raw
44+
: (stat.input_token.raw + stat.output_token.raw),
5045
}));
5146
} else {
52-
const days = parseInt(period);
47+
const days = Number(period);
5348
return sortedDaily.slice(-days).map((stat) => ({
5449
date: dayjs(stat.date).format('MM/DD'),
5550
[dataKey]: chartMetricType === 'cost'
5651
? stat.total_cost.raw
5752
: chartMetricType === 'count'
58-
? (stat.request_success.raw + stat.request_failed.raw)
59-
: (stat.input_token.raw + stat.output_token.raw),
53+
? (stat.request_success.raw + stat.request_failed.raw)
54+
: (stat.input_token.raw + stat.output_token.raw),
6055
}));
6156
}
6257
}, [sortedDaily, statsHourly, period, chartMetricType]);
6358

6459
const totals = useMemo(() => {
6560
if (period === '1') {
66-
if (!statsHourly) return { primary: 0, requests: 0, cost: 0, tokens: 0 };
61+
if (!statsHourly) return { requests: 0, cost: 0, tokens: 0 };
6762
const requests = statsHourly.reduce((acc, stat) => acc + stat.request_count.raw, 0);
6863
const cost = statsHourly.reduce((acc, stat) => acc + stat.total_cost.raw, 0);
6964
const tokens = statsHourly.reduce((acc, stat) => acc + stat.input_token.raw + stat.output_token.raw, 0);
7065
return {
71-
primary: chartMetricType === 'cost' ? cost : chartMetricType === 'count' ? requests : tokens,
7266
requests,
7367
cost,
7468
tokens,
7569
};
7670
} else {
77-
const days = parseInt(period);
71+
const days = Number(period);
7872
const recentStats = sortedDaily.slice(-days);
7973
const requests = recentStats.reduce((acc, stat) => acc + stat.request_success.raw + stat.request_failed.raw, 0);
8074
const cost = recentStats.reduce((acc, stat) => acc + stat.total_cost.raw, 0);
8175
const tokens = recentStats.reduce((acc, stat) => acc + stat.input_token.raw + stat.output_token.raw, 0);
8276
return {
83-
primary: chartMetricType === 'cost' ? cost : chartMetricType === 'count' ? requests : tokens,
8477
requests,
8578
cost,
8679
tokens,
8780
};
8881
}
89-
}, [sortedDaily, statsHourly, period, chartMetricType]);
82+
}, [sortedDaily, statsHourly, period]);
9083

9184
const chartConfig = useMemo(() => {
9285
const dataKey = getChartDataKey(chartMetricType);
@@ -100,7 +93,7 @@ export function StatsChart() {
10093
};
10194
}, [chartMetricType, t]);
10295

103-
const getPeriodLabel = (p: typeof period) => {
96+
const getPeriodLabel = (p: ChartPeriod) => {
10497
const labels = {
10598
'1': t('period.today'),
10699
'7': t('period.last7Days'),
@@ -109,26 +102,13 @@ export function StatsChart() {
109102
return labels[p];
110103
};
111104

112-
const getMetricLabel = (type: ChartMetricType) => {
113-
const labels = {
114-
'cost': t('metricType.cost'),
115-
'count': t('metricType.count'),
116-
'tokens': t('metricType.tokens'),
117-
};
118-
return labels[type];
119-
};
120105

121106
const handlePeriodClick = () => {
122-
const currentIndex = PERIODS.indexOf(period as typeof PERIODS[number]);
107+
const currentIndex = PERIODS.indexOf(period);
123108
const nextIndex = (currentIndex + 1) % PERIODS.length;
124109
setChartPeriod(PERIODS[nextIndex]);
125110
};
126111

127-
const getMetricValue = (type: ChartMetricType) => {
128-
if (type === 'cost') return formatMoney(totals.cost);
129-
if (type === 'count') return formatCount(totals.requests);
130-
return formatCount(totals.tokens);
131-
};
132112

133113
const getChartStroke = (type: ChartMetricType) => {
134114
if (type === 'cost') return 'var(--chart-1)';
@@ -144,8 +124,7 @@ export function StatsChart() {
144124

145125
return (
146126
<div className="rounded-3xl bg-card border-card-border border pt-4 pb-0 text-card-foreground custom-shadow">
147-
<div className="px-4 pb-2 space-y-4">
148-
{/* 第一行:标题 + 类型选择 */}
127+
<div className="px-4 pb-2 space-y-2">
149128
<div className="flex justify-between items-center">
150129
<h3 className="font-semibold text-base">{t('title')}</h3>
151130
<Tabs value={chartMetricType} onValueChange={(value) => setChartMetricType(value as ChartMetricType)}>
@@ -156,7 +135,7 @@ export function StatsChart() {
156135
</TabsList>
157136
</Tabs>
158137
</div>
159-
138+
160139
{/* 第二行:汇总统计 + 周期选择 */}
161140
<div className="flex justify-between items-start">
162141
<div className="flex gap-2 text-sm">
@@ -190,7 +169,7 @@ export function StatsChart() {
190169
>
191170
<div>
192171
<div className="text-xs text-muted-foreground">{t('timePeriod')}</div>
193-
<div className="text-base font-semibold">{getPeriodLabel(period as typeof PERIODS[number])}</div>
172+
<div className="text-base font-semibold">{getPeriodLabel(period)}</div>
194173
</div>
195174
</div>
196175
</div>
@@ -228,11 +207,11 @@ export function StatsChart() {
228207
}}
229208
/>
230209
<ChartTooltip cursor={false} content={<ChartTooltipContent indicator="line" />} />
231-
<Area
232-
type="monotone"
233-
dataKey={getChartDataKey(chartMetricType)}
234-
stroke={getChartStroke(chartMetricType)}
235-
fill={getChartFill(chartMetricType)}
210+
<Area
211+
type="monotone"
212+
dataKey={getChartDataKey(chartMetricType)}
213+
stroke={getChartStroke(chartMetricType)}
214+
fill={getChartFill(chartMetricType)}
236215
/>
237216
</AreaChart>
238217
</ChartContainer>

web/src/components/modules/home/rank.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,15 @@ import { useMemo } from 'react';
55
import { useTranslations } from 'next-intl';
66
import { TrendingUp } from 'lucide-react';
77
import { Tabs, TabsList, TabsTrigger, TabsContents, TabsContent } from '@/components/animate-ui/components/animate/tabs';
8-
import { useToolbarViewOptionsStore } from '@/components/modules/toolbar/view-options-store';
8+
import { useHomeViewStore, type RankSortMode } from '@/components/modules/home/store';
99

10-
type SortMode = 'cost' | 'count' | 'tokens';
1110
type ChannelData = NonNullable<ReturnType<typeof useChannelList>['data']>[number];
1211

1312
export function Rank() {
1413
const { data: channelData } = useChannelList();
1514
const t = useTranslations('home.rank');
16-
const rankSortMode = useToolbarViewOptionsStore((state) => state.rankSortMode);
17-
const setRankSortMode = useToolbarViewOptionsStore((state) => state.setRankSortMode);
15+
const rankSortMode = useHomeViewStore((state) => state.rankSortMode);
16+
const setRankSortMode = useHomeViewStore((state) => state.setRankSortMode);
1817

1918
const rankedByCost = useMemo<ChannelData[]>(() => {
2019
if (!channelData) return [];
@@ -40,7 +39,7 @@ export function Rank() {
4039
}
4140
};
4241

43-
const renderList = (channels: ChannelData[], mode: SortMode) => {
42+
const renderList = (channels: ChannelData[], mode: RankSortMode) => {
4443
if (channels.length === 0) {
4544
return (
4645
<div className="flex flex-col items-center justify-center py-8 text-muted-foreground">
@@ -123,7 +122,7 @@ export function Rank() {
123122

124123
return (
125124
<div className="rounded-3xl bg-card text-card-foreground border-card-border border p-4">
126-
<Tabs value={rankSortMode} onValueChange={(value) => setRankSortMode(value as SortMode)}>
125+
<Tabs value={rankSortMode} onValueChange={(value) => setRankSortMode(value as RankSortMode)}>
127126
<div className="flex items-center justify-between">
128127
<h3 className="font-semibold text-base">{t('title')}</h3>
129128
<TabsList>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use client';
2+
3+
import { create } from 'zustand';
4+
import { createJSONStorage, persist } from 'zustand/middleware';
5+
6+
export type RankSortMode = 'cost' | 'count' | 'tokens';
7+
export type ChartMetricType = 'cost' | 'count' | 'tokens';
8+
export type ChartPeriod = '1' | '7' | '30';
9+
10+
interface HomeViewState {
11+
rankSortMode: RankSortMode;
12+
chartMetricType: ChartMetricType;
13+
chartPeriod: ChartPeriod;
14+
setRankSortMode: (value: RankSortMode) => void;
15+
setChartMetricType: (value: ChartMetricType) => void;
16+
setChartPeriod: (value: ChartPeriod) => void;
17+
}
18+
19+
export const useHomeViewStore = create<HomeViewState>()(
20+
persist(
21+
(set) => ({
22+
rankSortMode: 'cost',
23+
chartMetricType: 'cost',
24+
chartPeriod: '1',
25+
setRankSortMode: (value) => set({ rankSortMode: value }),
26+
setChartMetricType: (value) => set({ chartMetricType: value }),
27+
setChartPeriod: (value) => set({ chartPeriod: value }),
28+
}),
29+
{
30+
name: 'home-view-options-storage',
31+
storage: createJSONStorage(() => localStorage),
32+
partialize: (state) => ({
33+
rankSortMode: state.rankSortMode,
34+
chartMetricType: state.chartMetricType,
35+
chartPeriod: state.chartPeriod,
36+
}),
37+
}
38+
)
39+
);

web/src/components/modules/toolbar/view-options-store.ts

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@ export type ToolbarPage = (typeof TOOLBAR_PAGES)[number];
1010
export type ChannelFilter = 'all' | 'enabled' | 'disabled';
1111
export type GroupFilter = 'all' | 'with-members' | 'empty';
1212
export type ModelFilter = 'all' | 'priced' | 'free';
13-
export type RankSortMode = 'cost' | 'count' | 'tokens';
14-
export type ChartMetricType = 'cost' | 'count' | 'tokens';
15-
export type ChartPeriod = '1' | '7' | '30';
1613

1714
interface ToolbarViewOptionsState {
1815
layouts: Partial<Record<ToolbarPage, ToolbarLayout>>;
@@ -21,9 +18,6 @@ interface ToolbarViewOptionsState {
2118
channelFilter: ChannelFilter;
2219
groupFilter: GroupFilter;
2320
modelFilter: ModelFilter;
24-
rankSortMode: RankSortMode;
25-
chartMetricType: ChartMetricType;
26-
chartPeriod: ChartPeriod;
2721

2822
getLayout: (item: ToolbarPage) => ToolbarLayout;
2923
setLayout: (item: ToolbarPage, value: ToolbarLayout) => void;
@@ -41,9 +35,6 @@ interface ToolbarViewOptionsState {
4135
setChannelFilter: (value: ChannelFilter) => void;
4236
setGroupFilter: (value: GroupFilter) => void;
4337
setModelFilter: (value: ModelFilter) => void;
44-
setRankSortMode: (value: RankSortMode) => void;
45-
setChartMetricType: (value: ChartMetricType) => void;
46-
setChartPeriod: (value: ChartPeriod) => void;
4738
}
4839

4940
export const useToolbarViewOptionsStore = create<ToolbarViewOptionsState>()(
@@ -55,9 +46,6 @@ export const useToolbarViewOptionsStore = create<ToolbarViewOptionsState>()(
5546
channelFilter: 'all',
5647
groupFilter: 'all',
5748
modelFilter: 'all',
58-
rankSortMode: 'cost',
59-
chartMetricType: 'cost',
60-
chartPeriod: '1',
6149

6250
getLayout: (item) => get().layouts[item] || 'grid',
6351
setLayout: (item, value) => {
@@ -80,9 +68,6 @@ export const useToolbarViewOptionsStore = create<ToolbarViewOptionsState>()(
8068
setChannelFilter: (value) => set({ channelFilter: value }),
8169
setGroupFilter: (value) => set({ groupFilter: value }),
8270
setModelFilter: (value) => set({ modelFilter: value }),
83-
setRankSortMode: (value) => set({ rankSortMode: value }),
84-
setChartMetricType: (value) => set({ chartMetricType: value }),
85-
setChartPeriod: (value) => set({ chartPeriod: value }),
8671
}),
8772
{
8873
name: 'toolbar-view-options-storage',
@@ -93,9 +78,6 @@ export const useToolbarViewOptionsStore = create<ToolbarViewOptionsState>()(
9378
channelFilter: state.channelFilter,
9479
groupFilter: state.groupFilter,
9580
modelFilter: state.modelFilter,
96-
rankSortMode: state.rankSortMode,
97-
chartMetricType: state.chartMetricType,
98-
chartPeriod: state.chartPeriod,
9981
}),
10082
}
10183
)

0 commit comments

Comments
 (0)