mirror of
https://github.com/chaitin/MonkeyCode.git
synced 2026-02-10 02:33:36 +08:00
@@ -34,15 +34,34 @@ type Statistics struct {
|
||||
}
|
||||
|
||||
type StatisticsFilter struct {
|
||||
Precision string `json:"precision" query:"precision" validate:"required,oneof=hour day" default:"day"` // 精度: "hour", "day"
|
||||
Duration int `json:"duration" query:"duration" validate:"gte=24,lte=90" default:"90"` // 持续时间 (小时或天数)`
|
||||
UserID string `json:"user_id,omitempty" query:"user_id"` // 用户ID,可选参数
|
||||
Precision string `json:"precision" query:"precision"` // 精度: "hour", "day"
|
||||
Duration int `json:"duration" query:"duration"` // 持续时间 (小时或天数)`
|
||||
StartAt int64 `json:"start_at" query:"start_at"` // 开始时间, 时间范围优先级高于精度选择
|
||||
EndAt int64 `json:"end_at" query:"end_at"` // 结束时间, 时间范围优先级高于精度选择
|
||||
UserID string `json:"user_id,omitempty" query:"user_id"` // 用户ID,可选参数
|
||||
}
|
||||
|
||||
func (s StatisticsFilter) MustPrecision() string {
|
||||
if s.Precision == "" {
|
||||
return "day"
|
||||
}
|
||||
return s.Precision
|
||||
}
|
||||
|
||||
func (s StatisticsFilter) StartTime() time.Time {
|
||||
if s.StartAt > 0 {
|
||||
return time.Unix(s.StartAt, 0)
|
||||
}
|
||||
return time.Now().Add(-24 * time.Hour)
|
||||
}
|
||||
|
||||
func (s StatisticsFilter) EndTime() time.Time {
|
||||
if s.EndAt > 0 {
|
||||
return time.Unix(s.EndAt, 0)
|
||||
}
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
type UserHeatmapResp struct {
|
||||
MaxCount int64 `json:"max_count"`
|
||||
Points []*UserHeatmap `json:"points"`
|
||||
|
||||
@@ -31,6 +31,7 @@ func (d *DashboardRepo) CategoryStat(ctx context.Context, req domain.StatisticsF
|
||||
var cs []domain.CategoryPoint
|
||||
if err := d.db.Task.Query().
|
||||
Where(task.CreatedAtGTE(req.StartTime())).
|
||||
Where(task.CreatedAtLTE(req.EndTime())).
|
||||
Where(task.WorkModeNEQ("")).
|
||||
Modify(func(s *sql.Selector) {
|
||||
s.Select(
|
||||
@@ -45,6 +46,7 @@ func (d *DashboardRepo) CategoryStat(ctx context.Context, req domain.StatisticsF
|
||||
var ps []domain.CategoryPoint
|
||||
if err := d.db.Task.Query().
|
||||
Where(task.CreatedAtGTE(req.StartTime())).
|
||||
Where(task.CreatedAtLTE(req.EndTime())).
|
||||
Where(task.ProgramLanguageNEQ("")).
|
||||
Where(task.IsSuggested(true)).
|
||||
Modify(func(s *sql.Selector) {
|
||||
@@ -95,6 +97,7 @@ func (d *DashboardRepo) TimeStat(ctx context.Context, req domain.StatisticsFilte
|
||||
udv := make([]DateValue, 0)
|
||||
if err := d.db.Task.Query().
|
||||
Where(task.CreatedAtGTE(req.StartTime())).
|
||||
Where(task.CreatedAtLTE(req.EndTime())).
|
||||
Aggregate(func(s *sql.Selector) string {
|
||||
return sql.As("COUNT(DISTINCT user_id)", "count")
|
||||
}).
|
||||
@@ -106,9 +109,10 @@ func (d *DashboardRepo) TimeStat(ctx context.Context, req domain.StatisticsFilte
|
||||
ds := make([]DateValue, 0)
|
||||
if err := d.db.Task.Query().
|
||||
Where(task.CreatedAtGTE(req.StartTime())).
|
||||
Where(task.CreatedAtLTE(req.EndTime())).
|
||||
Modify(func(s *sql.Selector) {
|
||||
s.Select(
|
||||
sql.As(fmt.Sprintf("date_trunc('%s', created_at)", req.Precision), "date"),
|
||||
sql.As(fmt.Sprintf("date_trunc('%s', created_at)", req.MustPrecision()), "date"),
|
||||
sql.As("COUNT(DISTINCT user_id)", "user_count"),
|
||||
sql.As("COUNT(*) FILTER (WHERE model_type = 'llm')", "llm_count"),
|
||||
sql.As("COUNT(*) FILTER (WHERE is_suggested = true AND model_type = 'coder')", "code_count"),
|
||||
@@ -206,6 +210,7 @@ func (d *DashboardRepo) UserCodeRank(ctx context.Context, req domain.StatisticsF
|
||||
var rs []UserCodeRank
|
||||
if err := d.db.Task.Query().
|
||||
Where(task.CreatedAtGTE(req.StartTime())).
|
||||
Where(task.CreatedAtLTE(req.EndTime())).
|
||||
Where(task.IsAccept(true)).
|
||||
Modify(func(s *sql.Selector) {
|
||||
s.Select(
|
||||
@@ -252,6 +257,7 @@ func (d *DashboardRepo) UserEvents(ctx context.Context, req domain.StatisticsFil
|
||||
WithTaskRecords().
|
||||
Where(task.ModelType(consts.ModelTypeLLM)).
|
||||
Where(task.CreatedAtGTE(req.StartTime())).
|
||||
Where(task.CreatedAtLTE(req.EndTime())).
|
||||
Where(task.HasUserWith(user.ID(id))).
|
||||
Order(task.ByCreatedAt(sql.OrderDesc())).
|
||||
Limit(100).
|
||||
@@ -286,9 +292,10 @@ func (d *DashboardRepo) UserStat(ctx context.Context, req domain.StatisticsFilte
|
||||
if err := d.db.Task.Query().
|
||||
Where(task.HasUserWith(user.ID(id))).
|
||||
Where(task.CreatedAtGTE(req.StartTime())).
|
||||
Where(task.CreatedAtLTE(req.EndTime())).
|
||||
Modify(func(s *sql.Selector) {
|
||||
s.Select(
|
||||
sql.As(fmt.Sprintf("date_trunc('%s', created_at)", req.Precision), "date"),
|
||||
sql.As(fmt.Sprintf("date_trunc('%s', created_at)", req.MustPrecision()), "date"),
|
||||
sql.As("COUNT(DISTINCT user_id)", "user_count"),
|
||||
sql.As("COUNT(*) FILTER (WHERE model_type = 'llm')", "llm_count"),
|
||||
sql.As("COUNT(*) FILTER (WHERE is_suggested = true AND model_type = 'coder')", "code_count"),
|
||||
@@ -305,6 +312,7 @@ func (d *DashboardRepo) UserStat(ctx context.Context, req domain.StatisticsFilte
|
||||
var cs []domain.CategoryPoint
|
||||
if err := d.db.Task.Query().
|
||||
Where(task.CreatedAtGTE(req.StartTime())).
|
||||
Where(task.CreatedAtLTE(req.EndTime())).
|
||||
Where(task.HasUserWith(user.ID(id))).
|
||||
Where(task.WorkModeNEQ("")).
|
||||
Modify(func(s *sql.Selector) {
|
||||
@@ -321,6 +329,7 @@ func (d *DashboardRepo) UserStat(ctx context.Context, req domain.StatisticsFilte
|
||||
var ps []domain.CategoryPoint
|
||||
if err := d.db.Task.Query().
|
||||
Where(task.CreatedAtGTE(req.StartTime())).
|
||||
Where(task.CreatedAtLTE(req.EndTime())).
|
||||
Where(task.HasUserWith(user.ID(id))).
|
||||
Where(task.ProgramLanguageNEQ("")).
|
||||
Where(task.IsSuggested(true)).
|
||||
|
||||
@@ -217,58 +217,7 @@ const parseDateInput = (input: string) => {
|
||||
};
|
||||
|
||||
const filterPresets = (obj: Record<string, any>, search: string) => {
|
||||
if (!search) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const searchWords = search.toLowerCase().split("-").filter(Boolean);
|
||||
|
||||
const filtered = Object.fromEntries(
|
||||
Object.entries(obj).filter(([_, value]) => {
|
||||
const keyLower = value.text.toLowerCase();
|
||||
return searchWords.every(word => keyLower.includes(word));
|
||||
})
|
||||
);
|
||||
|
||||
if (Object.entries(filtered).length > 0) {
|
||||
return filtered;
|
||||
}
|
||||
|
||||
const parsed = parseDateInput(search);
|
||||
if (parsed) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
const numberMatch = search.match(/\d+/);
|
||||
if (!numberMatch) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const n = parseInt(numberMatch[0], 10);
|
||||
const now = new Date();
|
||||
|
||||
return {
|
||||
[`last-${n}-days`]: {
|
||||
text: `Last ${n} Days`,
|
||||
start: startOfDay(subDays(now, n)),
|
||||
end: endOfDay(now)
|
||||
},
|
||||
[`last-${n}-weeks`]: {
|
||||
text: `Last ${n} Weeks`,
|
||||
start: startOfDay(subWeeks(now, n)),
|
||||
end: endOfDay(now)
|
||||
},
|
||||
[`last-${n}-months`]: {
|
||||
text: `Last ${n} Months`,
|
||||
start: startOfDay(subMonths(now, n)),
|
||||
end: endOfDay(now)
|
||||
},
|
||||
[`last-${n}-years`]: {
|
||||
text: `Last ${n} Years`,
|
||||
start: startOfDay(subYears(now, n)),
|
||||
end: endOfDay(now)
|
||||
}
|
||||
};
|
||||
return obj
|
||||
};
|
||||
|
||||
const formatDateRange = (start: Date, end: Date, timezone: string) => {
|
||||
|
||||
@@ -7,9 +7,8 @@ import {
|
||||
import { SecondTimeRange } from '@/components/ui/calendar';
|
||||
import {
|
||||
getRecent60MinutesData,
|
||||
getRecentDaysData,
|
||||
getRecent24HoursData as getRecentHoursData,
|
||||
getTimeRange,
|
||||
getRangeData,
|
||||
} from '@/utils';
|
||||
import { Grid2 as Grid, styled } from '@mui/material';
|
||||
import { useRequest } from 'ahooks';
|
||||
@@ -55,15 +54,6 @@ const GlobalStatistic = ({
|
||||
refreshDeps: [timeDuration],
|
||||
});
|
||||
const precision = useMemo(() => getTimeRange(timeDuration), [timeDuration]);
|
||||
const getRangeData = (
|
||||
data: Record<string, number>[],
|
||||
precision: 'day' | 'hour',
|
||||
label: { keyLabel?: string; valueLabel?: string } = { valueLabel: 'value' }
|
||||
) => {
|
||||
return precision === 'day'
|
||||
? getRecentDaysData(data, label)
|
||||
: getRecentHoursData(data, label);
|
||||
};
|
||||
|
||||
const {
|
||||
userActiveChartData,
|
||||
@@ -83,12 +73,12 @@ const GlobalStatistic = ({
|
||||
} = timeStatData || {};
|
||||
const label = { valueLabel: 'value' };
|
||||
return {
|
||||
userActiveChartData: getRangeData(active_users, precision, label),
|
||||
chatChartData: getRangeData(chats, precision, label),
|
||||
codeCompletionChartData: getRangeData(code_completions, precision, label),
|
||||
codeLineChartData: getRangeData(lines_of_code, precision, label),
|
||||
userActiveChartData: getRangeData(timeDuration, active_users, precision, label),
|
||||
chatChartData: getRangeData(timeDuration, chats, precision, label),
|
||||
codeCompletionChartData: getRangeData(timeDuration, code_completions, precision, label),
|
||||
codeLineChartData: getRangeData(timeDuration, lines_of_code, precision, label),
|
||||
realTimeTokenChartData: getRecent60MinutesData(real_time_tokens, label),
|
||||
acceptedPerChartData: getRangeData(accepted_per, precision, label),
|
||||
acceptedPerChartData: getRangeData(timeDuration, accepted_per, precision, label),
|
||||
};
|
||||
}, [timeStatData, precision]);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from '@/api/Dashboard';
|
||||
import { DomainUser } from '@/api/types';
|
||||
import { SecondTimeRange } from '@/components/ui/calendar';
|
||||
import { getRecent24HoursData, getRecentDaysData, getTimeRange } from '@/utils';
|
||||
import { getRangeData, getRecent24HoursData, getRecentDaysData, getTimeRange } from '@/utils';
|
||||
import { Grid2 as Grid } from '@mui/material';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { useMemo } from 'react';
|
||||
@@ -16,8 +16,6 @@ import MemberInfo from './memberInfo';
|
||||
import PieCharts from './pieCharts';
|
||||
import { RecentActivityCard } from './statisticCard';
|
||||
|
||||
type Precision = 'day' | 'hour';
|
||||
|
||||
const MemberStatistic = ({
|
||||
memberData,
|
||||
userList,
|
||||
@@ -67,16 +65,6 @@ const MemberStatistic = ({
|
||||
}
|
||||
);
|
||||
|
||||
const getRangeData = (
|
||||
data: Record<string, number>[],
|
||||
precision: Precision,
|
||||
label: { keyLabel?: string; valueLabel?: string } = { valueLabel: 'value' }
|
||||
) => {
|
||||
return precision === 'day'
|
||||
? getRecentDaysData(data, label)
|
||||
: getRecent24HoursData(data, label);
|
||||
};
|
||||
|
||||
const {
|
||||
chatChartData,
|
||||
codeCompletionChartData,
|
||||
@@ -90,14 +78,15 @@ const MemberStatistic = ({
|
||||
lines_of_code = [],
|
||||
} = userStat || {};
|
||||
const label = { valueLabel: 'value' };
|
||||
const chatChartData = getRangeData(chats, precision, label);
|
||||
const chatChartData = getRangeData(timeDuration, chats, precision, label);
|
||||
const codeCompletionChartData = getRangeData(
|
||||
timeDuration,
|
||||
code_completions,
|
||||
precision,
|
||||
label
|
||||
);
|
||||
const codeLineChartData = getRangeData(lines_of_code, precision, label);
|
||||
const acceptedPerChartData = getRangeData(accepted_per, precision, label);
|
||||
const codeLineChartData = getRangeData(timeDuration, lines_of_code, precision, label);
|
||||
const acceptedPerChartData = getRangeData(timeDuration, accepted_per, precision, label);
|
||||
return {
|
||||
chatChartData,
|
||||
codeCompletionChartData,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { getListUser } from '@/api/User';
|
||||
import { Stack, MenuItem, Select, Box } from '@mui/material';
|
||||
import { Stack, Box } from '@mui/material';
|
||||
import { CusTabs } from '@c-x/ui';
|
||||
import GlobalStatistic from './components/globalStatistic';
|
||||
import { useRequest } from 'ahooks';
|
||||
@@ -10,14 +10,16 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { DomainUser } from '@/api/types';
|
||||
import { v1LicenseList } from '@/api';
|
||||
import { Calendar, RangeValue } from '@/components/ui/calendar';
|
||||
import { endOfDay, startOfDay, subDays, subMonths, subWeeks } from 'date-fns';
|
||||
import { subDays, subMonths, subWeeks } from 'date-fns';
|
||||
import { getTimeRange } from '@/utils';
|
||||
|
||||
const get24HoursRange = () => {
|
||||
return {
|
||||
start: startOfDay(subDays(new Date(), 1)),
|
||||
end: endOfDay(new Date()),
|
||||
start: subDays(new Date(), 1),
|
||||
end: new Date(),
|
||||
};
|
||||
};
|
||||
|
||||
const presets = {
|
||||
'last-1-days': {
|
||||
text: '最近 24 小时',
|
||||
@@ -25,23 +27,38 @@ const presets = {
|
||||
},
|
||||
'last-3-days': {
|
||||
text: '最近 3 天',
|
||||
start: startOfDay(subDays(new Date(), 3)),
|
||||
end: endOfDay(new Date()),
|
||||
start: subDays(new Date(), 3),
|
||||
end: new Date(),
|
||||
},
|
||||
'last-7-days': {
|
||||
text: '最近 7 天',
|
||||
start: startOfDay(subWeeks(new Date(), 1)),
|
||||
end: endOfDay(new Date()),
|
||||
start: subWeeks(new Date(), 1),
|
||||
end: new Date(),
|
||||
},
|
||||
'last-14-days': {
|
||||
text: '最近 14 天',
|
||||
start: startOfDay(subWeeks(new Date(), 2)),
|
||||
end: endOfDay(new Date()),
|
||||
start: subWeeks(new Date(), 2),
|
||||
end: new Date(),
|
||||
},
|
||||
'last-month': {
|
||||
text: '最近 1 月',
|
||||
start: startOfDay(subMonths(new Date(), 1)),
|
||||
end: endOfDay(new Date()),
|
||||
start: subMonths(new Date(), 1),
|
||||
end: new Date(),
|
||||
},
|
||||
'last-3-months': {
|
||||
text: '最近 3 月',
|
||||
start: subMonths(new Date(), 3),
|
||||
end: new Date(),
|
||||
},
|
||||
'last-half-year': {
|
||||
text: '最近半年',
|
||||
start: subMonths(new Date(), 6),
|
||||
end: new Date(),
|
||||
},
|
||||
'last-year': {
|
||||
text: '最近一年',
|
||||
start: subMonths(new Date(), 12),
|
||||
end: new Date(),
|
||||
},
|
||||
};
|
||||
export type TimeRange = '90d' | '24h';
|
||||
@@ -96,22 +113,31 @@ const Dashboard = () => {
|
||||
};
|
||||
const handleTimeRangeChange = (value: any) => {
|
||||
if (value) {
|
||||
console.log(value)
|
||||
setTimeRange(value);
|
||||
} else {
|
||||
setTimeRange(get24HoursRange());
|
||||
}
|
||||
};
|
||||
|
||||
const secondValue = useMemo(() => {
|
||||
return {
|
||||
start_at: timeRange.start
|
||||
? Math.floor(timeRange.start?.getTime() / 1000)
|
||||
: 0,
|
||||
end_at: timeRange.end ? Math.floor(timeRange.end?.getTime() / 1000) : 0,
|
||||
precision: getTimeRange({
|
||||
start_at: timeRange.start
|
||||
? Math.floor(timeRange.start?.getTime() / 1000)
|
||||
: 0,
|
||||
end_at: timeRange.end ? Math.floor(timeRange.end?.getTime() / 1000) : 0,
|
||||
}),
|
||||
};
|
||||
}, [timeRange]);
|
||||
|
||||
return (
|
||||
<Stack gap={2} sx={{ height: '100%' }}>
|
||||
<Stack direction='row' gap={2} alignItems='center'>
|
||||
<Stack direction='row' gap={2} justifyContent='space-between' alignItems='center'>
|
||||
<CusTabs
|
||||
value={tabValue}
|
||||
onChange={onTabChange}
|
||||
@@ -137,10 +163,11 @@ const Dashboard = () => {
|
||||
/>
|
||||
<Box sx={{ py: '4px', pr: 5 }}>
|
||||
<Calendar
|
||||
isDocsPage
|
||||
allowClear
|
||||
disabled={license?.edition !== 2}
|
||||
onChange={handleTimeRangeChange}
|
||||
presets={presets}
|
||||
presetIndex={0}
|
||||
value={timeRange}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -157,7 +157,8 @@ export const getRedirectUrl = (source: 'user' | 'admin' = 'admin') => {
|
||||
|
||||
export const getRecentDaysData = (
|
||||
data: Record<string, number>[] = [],
|
||||
label: { keyLabel?: string; valueLabel?: string } = {}
|
||||
label: { keyLabel?: string; valueLabel?: string } = {},
|
||||
durationDay: number = 90
|
||||
) => {
|
||||
const { keyLabel = 'timestamp', valueLabel = 'tokens' } = label;
|
||||
const xData: string[] = [];
|
||||
@@ -169,7 +170,7 @@ export const getRecentDaysData = (
|
||||
item[valueLabel]!;
|
||||
});
|
||||
|
||||
for (let i = 0; i < 90; i++) {
|
||||
for (let i = 0; i < durationDay; i++) {
|
||||
const time = dayjs().startOf('day').subtract(i, 'day').format('YYYY-MM-DD');
|
||||
if (dateMap[time]) {
|
||||
xData.unshift(time);
|
||||
@@ -269,8 +270,60 @@ export const getBaseLanguageId = (languageId: string): string => {
|
||||
|
||||
export const getTimeRange = (timeDuration: SecondTimeRange) => {
|
||||
const diff = timeDuration.end_at - timeDuration.start_at;
|
||||
if (diff > 24 * 60 * 60 * 1000) {
|
||||
if (diff > 365 * 24 * 60 * 60) {
|
||||
return 'month';
|
||||
}
|
||||
if (diff > 17* 24 * 60 * 60) {
|
||||
return 'day';
|
||||
}
|
||||
return 'hour';
|
||||
};
|
||||
|
||||
export const getRangeData = (
|
||||
timeDuration: { start_at: number; end_at: number },
|
||||
data: { timestamp?: number; value?: number }[],
|
||||
precision: 'month' | 'day' | 'hour',
|
||||
label: { keyLabel?: string; valueLabel?: string } = { keyLabel: 'timestamp', valueLabel: 'value' }
|
||||
) => {
|
||||
const { keyLabel = 'timestamp', valueLabel = 'value' } = label;
|
||||
const xData: (string | number)[] = [];
|
||||
const yData: number[] = [];
|
||||
const dateMap: Record<string, number> = {};
|
||||
|
||||
// 将原始数据转换为时间映射
|
||||
data.forEach((item) => {
|
||||
const timestampValue = item[keyLabel as keyof typeof item] as number;
|
||||
const dataValue = item[valueLabel as keyof typeof item] as number;
|
||||
|
||||
if (timestampValue && dataValue !== undefined) {
|
||||
// API返回的timestamp是秒,直接使用dayjs.unix()
|
||||
const timeKey = precision === 'day'
|
||||
? dayjs.unix(timestampValue).format('YYYY-MM-DD')
|
||||
: dayjs.unix(timestampValue).format('YYYY-MM-DD HH:00');
|
||||
dateMap[timeKey] = dataValue;
|
||||
}
|
||||
});
|
||||
|
||||
// timeDuration中的时间戳是毫秒,需要除以1000
|
||||
const startTime = dayjs.unix(timeDuration.start_at);
|
||||
const endTime = dayjs.unix(timeDuration.end_at);
|
||||
|
||||
// 根据 precision 对齐时间点
|
||||
const startPoint = precision === 'day' ? startTime.startOf('day') : startTime.startOf('hour');
|
||||
const endPoint = precision === 'day' ? endTime.startOf('day') : endTime.startOf('hour');
|
||||
|
||||
// 从开始时间到结束时间,逐个补齐数据点
|
||||
let currentTime = startPoint;
|
||||
while (currentTime.unix() <= endPoint.unix()) {
|
||||
const timeKey = precision === 'day'
|
||||
? currentTime.format('YYYY-MM-DD')
|
||||
: currentTime.format('YYYY-MM-DD HH:00');
|
||||
|
||||
xData.push(timeKey);
|
||||
yData.push(dateMap[timeKey] || 0);
|
||||
|
||||
currentTime = currentTime.add(1, precision);
|
||||
}
|
||||
|
||||
return { xData, yData };
|
||||
};
|
||||
Reference in New Issue
Block a user