✨ feat: add admin statistics (#59)
This commit is contained in:
parent
f2aafab0d9
commit
332b6fd397
117
controller/analytics.go
Normal file
117
controller/analytics.go
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"one-api/model"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetUserStatistics(c *gin.Context) {
|
||||||
|
userStatistics, err := model.GetStatisticsUser()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无法获取用户统计信息.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": userStatistics,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetChannelStatistics(c *gin.Context) {
|
||||||
|
channelStatistics, err := model.GetStatisticsChannel()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无法获取渠道统计信息.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": channelStatistics,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRedemptionStatistics(c *gin.Context) {
|
||||||
|
redemptionStatistics, err := model.GetStatisticsRedemption()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无法获取充值卡统计信息.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": redemptionStatistics,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserStatisticsByPeriod(c *gin.Context) {
|
||||||
|
startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
|
||||||
|
endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
|
||||||
|
logStatistics, err := model.GetUserStatisticsByPeriod(startTimestamp, endTimestamp)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无法获取用户区间统计信息.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": logStatistics,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetChannelExpensesByPeriod(c *gin.Context) {
|
||||||
|
startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
|
||||||
|
endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
|
||||||
|
logStatistics, err := model.GetChannelExpensesByPeriod(startTimestamp, endTimestamp)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无法获取渠道区间统计信息.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": logStatistics,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRedemptionStatisticsByPeriod(c *gin.Context) {
|
||||||
|
startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
|
||||||
|
endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
|
||||||
|
logStatistics, err := model.GetStatisticsRedemptionByPeriod(startTimestamp, endTimestamp)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无法获取充值区间统计信息.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": logStatistics,
|
||||||
|
})
|
||||||
|
}
|
@ -253,7 +253,7 @@ func GetUserDashboard(c *gin.Context) {
|
|||||||
endOfDay := toDay.Add(time.Hour * 24).Add(-time.Second).Unix()
|
endOfDay := toDay.Add(time.Hour * 24).Add(-time.Second).Unix()
|
||||||
startOfDay := toDay.AddDate(0, 0, -7).Unix()
|
startOfDay := toDay.AddDate(0, 0, -7).Unix()
|
||||||
|
|
||||||
dashboards, err := model.SearchLogsByDayAndModel(id, int(startOfDay), int(endOfDay))
|
dashboards, err := model.GetUserModelExpensesByPeriod(id, int(startOfDay), int(endOfDay))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
|
@ -182,3 +182,13 @@ func DeleteDisabledChannel() (int64, error) {
|
|||||||
result := DB.Where("status = ? or status = ?", common.ChannelStatusAutoDisabled, common.ChannelStatusManuallyDisabled).Delete(&Channel{})
|
result := DB.Where("status = ? or status = ?", common.ChannelStatusAutoDisabled, common.ChannelStatusManuallyDisabled).Delete(&Channel{})
|
||||||
return result.RowsAffected, result.Error
|
return result.RowsAffected, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ChannelStatistics struct {
|
||||||
|
TotalChannels int `json:"total_channels"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetStatisticsChannel() (statistics []*ChannelStatistics, err error) {
|
||||||
|
err = DB.Table("channels").Select("count(*) as total_channels, status").Group("status").Scan(&statistics).Error
|
||||||
|
return statistics, err
|
||||||
|
}
|
||||||
|
37
model/common.go
Normal file
37
model/common.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"one-api/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getDateFormat(groupType string) string {
|
||||||
|
var dateFormat string
|
||||||
|
if groupType == "day" {
|
||||||
|
dateFormat = "%Y-%m-%d"
|
||||||
|
if common.UsingPostgreSQL {
|
||||||
|
dateFormat = "YYYY-MM-DD"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dateFormat = "%Y-%m"
|
||||||
|
if common.UsingPostgreSQL {
|
||||||
|
dateFormat = "YYYY-MM"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dateFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTimestampGroupsSelect(fieldName, groupType, alias string) string {
|
||||||
|
dateFormat := getDateFormat(groupType)
|
||||||
|
var groupSelect string
|
||||||
|
|
||||||
|
if common.UsingPostgreSQL {
|
||||||
|
groupSelect = fmt.Sprintf(`TO_CHAR(date_trunc('%s', to_timestamp(%s)), '%s') as %s`, groupType, fieldName, dateFormat, alias)
|
||||||
|
} else if common.UsingSQLite {
|
||||||
|
groupSelect = fmt.Sprintf(`strftime('%s', datetime(%s, 'unixepoch')) as %s`, dateFormat, fieldName, alias)
|
||||||
|
} else {
|
||||||
|
groupSelect = fmt.Sprintf(`DATE_FORMAT(FROM_UNIXTIME(%s), '%s') as %s`, fieldName, dateFormat, alias)
|
||||||
|
}
|
||||||
|
|
||||||
|
return groupSelect
|
||||||
|
}
|
64
model/log.go
64
model/log.go
@ -24,15 +24,6 @@ type Log struct {
|
|||||||
RequestTime int `json:"request_time" gorm:"default:0"`
|
RequestTime int `json:"request_time" gorm:"default:0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogStatistic struct {
|
|
||||||
Day string `gorm:"column:day"`
|
|
||||||
ModelName string `gorm:"column:model_name"`
|
|
||||||
RequestCount int `gorm:"column:request_count"`
|
|
||||||
Quota int `gorm:"column:quota"`
|
|
||||||
PromptTokens int `gorm:"column:prompt_tokens"`
|
|
||||||
CompletionTokens int `gorm:"column:completion_tokens"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
LogTypeUnknown = iota
|
LogTypeUnknown = iota
|
||||||
LogTypeTopup
|
LogTypeTopup
|
||||||
@ -195,16 +186,22 @@ func DeleteOldLog(targetTimestamp int64) (int64, error) {
|
|||||||
return result.RowsAffected, result.Error
|
return result.RowsAffected, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func SearchLogsByDayAndModel(user_id, start, end int) (LogStatistics []*LogStatistic, err error) {
|
type LogStatistic struct {
|
||||||
groupSelect := "DATE_FORMAT(FROM_UNIXTIME(created_at), '%Y-%m-%d') as day"
|
Date string `gorm:"column:date"`
|
||||||
|
RequestCount int64 `gorm:"column:request_count"`
|
||||||
|
Quota int64 `gorm:"column:quota"`
|
||||||
|
PromptTokens int64 `gorm:"column:prompt_tokens"`
|
||||||
|
CompletionTokens int64 `gorm:"column:completion_tokens"`
|
||||||
|
RequestTime int64 `gorm:"column:request_time"`
|
||||||
|
}
|
||||||
|
|
||||||
if common.UsingPostgreSQL {
|
type LogStatisticGroupModel struct {
|
||||||
groupSelect = "TO_CHAR(date_trunc('day', to_timestamp(created_at)), 'YYYY-MM-DD') as day"
|
LogStatistic
|
||||||
}
|
ModelName string `gorm:"column:model_name"`
|
||||||
|
}
|
||||||
|
|
||||||
if common.UsingSQLite {
|
func GetUserModelExpensesByPeriod(user_id, startTimestamp, endTimestamp int) (LogStatistic []*LogStatisticGroupModel, err error) {
|
||||||
groupSelect = "strftime('%Y-%m-%d', datetime(created_at, 'unixepoch')) as day"
|
groupSelect := getTimestampGroupsSelect("created_at", "day", "date")
|
||||||
}
|
|
||||||
|
|
||||||
err = DB.Raw(`
|
err = DB.Raw(`
|
||||||
SELECT `+groupSelect+`,
|
SELECT `+groupSelect+`,
|
||||||
@ -216,11 +213,36 @@ func SearchLogsByDayAndModel(user_id, start, end int) (LogStatistics []*LogStati
|
|||||||
WHERE type=2
|
WHERE type=2
|
||||||
AND user_id= ?
|
AND user_id= ?
|
||||||
AND created_at BETWEEN ? AND ?
|
AND created_at BETWEEN ? AND ?
|
||||||
GROUP BY day, model_name
|
GROUP BY date, model_name
|
||||||
ORDER BY day, model_name
|
ORDER BY date, model_name
|
||||||
`, user_id, start, end).Scan(&LogStatistics).Error
|
`, user_id, startTimestamp, endTimestamp).Scan(&LogStatistic).Error
|
||||||
|
|
||||||
fmt.Println(user_id, start, end)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogStatisticGroupChannel struct {
|
||||||
|
LogStatistic
|
||||||
|
Channel string `gorm:"column:channel"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetChannelExpensesByPeriod(startTimestamp, endTimestamp int64) (LogStatistics []*LogStatisticGroupChannel, err error) {
|
||||||
|
groupSelect := getTimestampGroupsSelect("created_at", "day", "date")
|
||||||
|
|
||||||
|
err = DB.Raw(`
|
||||||
|
SELECT `+groupSelect+`,
|
||||||
|
count(1) as request_count,
|
||||||
|
sum(quota) as quota,
|
||||||
|
sum(prompt_tokens) as prompt_tokens,
|
||||||
|
sum(completion_tokens) as completion_tokens,
|
||||||
|
sum(request_time) as request_time,
|
||||||
|
channels.name as channel
|
||||||
|
FROM logs
|
||||||
|
JOIN channels ON logs.channel_id = channels.id
|
||||||
|
WHERE logs.type=2
|
||||||
|
AND logs.created_at BETWEEN ? AND ?
|
||||||
|
GROUP BY date, channels.name
|
||||||
|
ORDER BY date, channels.name
|
||||||
|
`, startTimestamp, endTimestamp).Scan(&LogStatistics).Error
|
||||||
|
|
||||||
return LogStatistics, err
|
return LogStatistics, err
|
||||||
}
|
}
|
||||||
|
@ -115,3 +115,37 @@ func DeleteRedemptionById(id int) (err error) {
|
|||||||
}
|
}
|
||||||
return redemption.Delete()
|
return redemption.Delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RedemptionStatistics struct {
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
Quota int64 `json:"quota"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetStatisticsRedemption() (redemptionStatistics []*RedemptionStatistics, err error) {
|
||||||
|
err = DB.Model(&Redemption{}).Select("status", "count(*) as count", "sum(quota) as quota").Where("status != ?", 2).Group("status").Scan(&redemptionStatistics).Error
|
||||||
|
return redemptionStatistics, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type RedemptionStatisticsGroup struct {
|
||||||
|
Date string `json:"date"`
|
||||||
|
Quota int64 `json:"quota"`
|
||||||
|
UserCount int64 `json:"user_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetStatisticsRedemptionByPeriod(startTimestamp, endTimestamp int64) (redemptionStatistics []*RedemptionStatisticsGroup, err error) {
|
||||||
|
groupSelect := getTimestampGroupsSelect("redeemed_time", "day", "date")
|
||||||
|
|
||||||
|
err = DB.Raw(`
|
||||||
|
SELECT `+groupSelect+`,
|
||||||
|
sum(quota) as quota,
|
||||||
|
count(distinct user_id) as user_count
|
||||||
|
FROM redemptions
|
||||||
|
WHERE status=3
|
||||||
|
AND redeemed_time BETWEEN ? AND ?
|
||||||
|
GROUP BY date
|
||||||
|
ORDER BY date
|
||||||
|
`, startTimestamp, endTimestamp).Scan(&redemptionStatistics).Error
|
||||||
|
|
||||||
|
return redemptionStatistics, err
|
||||||
|
}
|
||||||
|
@ -29,6 +29,7 @@ type User struct {
|
|||||||
Group string `json:"group" gorm:"type:varchar(32);default:'default'"`
|
Group string `json:"group" gorm:"type:varchar(32);default:'default'"`
|
||||||
AffCode string `json:"aff_code" gorm:"type:varchar(32);column:aff_code;uniqueIndex"`
|
AffCode string `json:"aff_code" gorm:"type:varchar(32);column:aff_code;uniqueIndex"`
|
||||||
InviterId int `json:"inviter_id" gorm:"type:int;column:inviter_id;index"`
|
InviterId int `json:"inviter_id" gorm:"type:int;column:inviter_id;index"`
|
||||||
|
CreatedTime int64 `json:"created_time" gorm:"bigint"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetMaxUserId() int {
|
func GetMaxUserId() int {
|
||||||
@ -90,6 +91,7 @@ func (user *User) Insert(inviterId int) error {
|
|||||||
user.Quota = common.QuotaForNewUser
|
user.Quota = common.QuotaForNewUser
|
||||||
user.AccessToken = common.GetUUID()
|
user.AccessToken = common.GetUUID()
|
||||||
user.AffCode = common.GetRandomString(4)
|
user.AffCode = common.GetRandomString(4)
|
||||||
|
user.CreatedTime = common.GetTimestamp()
|
||||||
result := DB.Create(user)
|
result := DB.Create(user)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return result.Error
|
return result.Error
|
||||||
@ -365,3 +367,37 @@ func GetUsernameById(id int) (username string) {
|
|||||||
DB.Model(&User{}).Where("id = ?", id).Select("username").Find(&username)
|
DB.Model(&User{}).Where("id = ?", id).Select("username").Find(&username)
|
||||||
return username
|
return username
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StatisticsUser struct {
|
||||||
|
TotalQuota int64 `json:"total_quota"`
|
||||||
|
TotalUsedQuota int64 `json:"total_used_quota"`
|
||||||
|
TotalUser int64 `json:"total_user"`
|
||||||
|
TotalInviterUser int64 `json:"total_inviter_user"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetStatisticsUser() (statisticsUser *StatisticsUser, err error) {
|
||||||
|
err = DB.Model(&User{}).Select("sum(quota) as total_quota, sum(used_quota) as total_used_quota, count(*) as total_user, count(CASE WHEN inviter_id != 0 THEN 1 END) as total_inviter_user").Scan(&statisticsUser).Error
|
||||||
|
return statisticsUser, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserStatisticsByPeriod struct {
|
||||||
|
Date string `json:"date"`
|
||||||
|
UserCount int64 `json:"user_count"`
|
||||||
|
InviterUserCount int64 `json:"inviter_user_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserStatisticsByPeriod(startTimestamp, endTimestamp int64) (statistics []*UserStatisticsByPeriod, err error) {
|
||||||
|
groupSelect := getTimestampGroupsSelect("created_time", "day", "date")
|
||||||
|
|
||||||
|
err = DB.Raw(`
|
||||||
|
SELECT `+groupSelect+`,
|
||||||
|
count(*) as user_count,
|
||||||
|
count(CASE WHEN inviter_id != 0 THEN 1 END) as inviter_user_count
|
||||||
|
FROM users
|
||||||
|
WHERE created_time BETWEEN ? AND ?
|
||||||
|
GROUP BY date
|
||||||
|
ORDER BY date
|
||||||
|
`, startTimestamp, endTimestamp).Scan(&statistics).Error
|
||||||
|
|
||||||
|
return statistics, err
|
||||||
|
}
|
||||||
|
@ -112,5 +112,16 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
{
|
{
|
||||||
groupRoute.GET("/", controller.GetGroups)
|
groupRoute.GET("/", controller.GetGroups)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
analyticsRoute := apiRouter.Group("/analytics")
|
||||||
|
analyticsRoute.Use(middleware.AdminAuth())
|
||||||
|
{
|
||||||
|
analyticsRoute.GET("/user_statistics", controller.GetUserStatistics)
|
||||||
|
analyticsRoute.GET("/channel_statistics", controller.GetChannelStatistics)
|
||||||
|
analyticsRoute.GET("/redemption_statistics", controller.GetRedemptionStatistics)
|
||||||
|
analyticsRoute.GET("/users_period", controller.GetUserStatisticsByPeriod)
|
||||||
|
analyticsRoute.GET("/channel_period", controller.GetChannelExpensesByPeriod)
|
||||||
|
analyticsRoute.GET("/redemption_period", controller.GetRedemptionStatisticsByPeriod)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,11 +8,23 @@ import {
|
|||||||
IconKey,
|
IconKey,
|
||||||
IconGardenCart,
|
IconGardenCart,
|
||||||
IconUser,
|
IconUser,
|
||||||
IconUserScan
|
IconUserScan,
|
||||||
|
IconActivity
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
|
||||||
// constant
|
// constant
|
||||||
const icons = { IconDashboard, IconSitemap, IconArticle, IconCoin, IconAdjustments, IconKey, IconGardenCart, IconUser, IconUserScan };
|
const icons = {
|
||||||
|
IconDashboard,
|
||||||
|
IconSitemap,
|
||||||
|
IconArticle,
|
||||||
|
IconCoin,
|
||||||
|
IconAdjustments,
|
||||||
|
IconKey,
|
||||||
|
IconGardenCart,
|
||||||
|
IconUser,
|
||||||
|
IconUserScan,
|
||||||
|
IconActivity
|
||||||
|
};
|
||||||
|
|
||||||
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
|
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
|
||||||
|
|
||||||
@ -29,6 +41,15 @@ const panel = {
|
|||||||
breadcrumbs: false,
|
breadcrumbs: false,
|
||||||
isAdmin: false
|
isAdmin: false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'analytics',
|
||||||
|
title: '分析',
|
||||||
|
type: 'item',
|
||||||
|
url: '/panel/analytics',
|
||||||
|
icon: icons.IconActivity,
|
||||||
|
breadcrumbs: false,
|
||||||
|
isAdmin: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'channel',
|
id: 'channel',
|
||||||
title: '渠道',
|
title: '渠道',
|
||||||
|
@ -13,6 +13,7 @@ const Topup = Loadable(lazy(() => import('views/Topup')));
|
|||||||
const User = Loadable(lazy(() => import('views/User')));
|
const User = Loadable(lazy(() => import('views/User')));
|
||||||
const Profile = Loadable(lazy(() => import('views/Profile')));
|
const Profile = Loadable(lazy(() => import('views/Profile')));
|
||||||
const NotFoundView = Loadable(lazy(() => import('views/Error')));
|
const NotFoundView = Loadable(lazy(() => import('views/Error')));
|
||||||
|
const Analytics = Loadable(lazy(() => import('views/Analytics')));
|
||||||
|
|
||||||
// dashboard routing
|
// dashboard routing
|
||||||
const Dashboard = Loadable(lazy(() => import('views/Dashboard')));
|
const Dashboard = Loadable(lazy(() => import('views/Dashboard')));
|
||||||
@ -63,6 +64,10 @@ const MainRoutes = {
|
|||||||
path: 'profile',
|
path: 'profile',
|
||||||
element: <Profile />
|
element: <Profile />
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'analytics',
|
||||||
|
element: <Analytics />
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '404',
|
path: '404',
|
||||||
element: <NotFoundView />
|
element: <NotFoundView />
|
||||||
|
104
web/src/ui-component/DateRangePicker.js
Normal file
104
web/src/ui-component/DateRangePicker.js
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import { Stack, Typography } from '@mui/material';
|
||||||
|
import { LocalizationProvider, DatePicker } from '@mui/x-date-pickers';
|
||||||
|
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||||
|
require('dayjs/locale/zh-cn');
|
||||||
|
|
||||||
|
export default class DateRangePicker extends React.Component {
|
||||||
|
state = {
|
||||||
|
startDate: this.props.defaultValue.start,
|
||||||
|
endDate: this.props.defaultValue.end,
|
||||||
|
localeText: this.props.localeText,
|
||||||
|
startOpen: false,
|
||||||
|
endOpen: false,
|
||||||
|
views: this.props?.views
|
||||||
|
};
|
||||||
|
|
||||||
|
handleStartChange = (date) => {
|
||||||
|
// 将 date设置当天的 00:00:00
|
||||||
|
date = date.startOf('day');
|
||||||
|
this.setState({ startDate: date });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleEndChange = (date) => {
|
||||||
|
// 将 date设置当天的 23:59:59
|
||||||
|
date = date.endOf('day');
|
||||||
|
this.setState({ endDate: date });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleStartOpen = () => {
|
||||||
|
this.setState({ startOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleStartClose = () => {
|
||||||
|
this.setState({ startOpen: false, endOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleEndClose = () => {
|
||||||
|
this.setState({ endOpen: false }, () => {
|
||||||
|
const { startDate, endDate } = this.state;
|
||||||
|
const { defaultValue, onChange } = this.props;
|
||||||
|
if (!onChange) return;
|
||||||
|
if (startDate !== defaultValue.start || endDate !== defaultValue.end) {
|
||||||
|
onChange({ start: startDate, end: endDate });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { startOpen, endOpen, startDate, endDate, localeText } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack direction="row" spacing={2} alignItems="center">
|
||||||
|
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale={'zh-cn'}>
|
||||||
|
<DatePicker
|
||||||
|
label={localeText?.start || ''}
|
||||||
|
name="start_date"
|
||||||
|
defaultValue={startDate}
|
||||||
|
open={startOpen}
|
||||||
|
onChange={this.handleStartChange}
|
||||||
|
onOpen={this.handleStartOpen}
|
||||||
|
onClose={this.handleStartClose}
|
||||||
|
disableFuture
|
||||||
|
disableHighlightToday
|
||||||
|
slotProps={{
|
||||||
|
textField: {
|
||||||
|
readOnly: true,
|
||||||
|
onClick: this.handleStartOpen
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
views={this.views}
|
||||||
|
/>
|
||||||
|
<Typography variant="body"> – </Typography>
|
||||||
|
<DatePicker
|
||||||
|
label={localeText?.end || ''}
|
||||||
|
name="end_date"
|
||||||
|
defaultValue={endDate}
|
||||||
|
open={endOpen}
|
||||||
|
onChange={this.handleEndChange}
|
||||||
|
onOpen={this.handleStartOpen}
|
||||||
|
onClose={this.handleEndClose}
|
||||||
|
minDate={startDate}
|
||||||
|
disableFuture
|
||||||
|
disableHighlightToday
|
||||||
|
slotProps={{
|
||||||
|
textField: {
|
||||||
|
readOnly: true,
|
||||||
|
onClick: this.handleStartOpen
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
views={this.views}
|
||||||
|
/>
|
||||||
|
</LocalizationProvider>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DateRangePicker.propTypes = {
|
||||||
|
defaultValue: PropTypes.object,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
localeText: PropTypes.object,
|
||||||
|
views: PropTypes.array
|
||||||
|
};
|
41
web/src/ui-component/cards/DataCard.js
Normal file
41
web/src/ui-component/cards/DataCard.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import SubCard from 'ui-component/cards/SubCard';
|
||||||
|
import { Typography, Tooltip, Divider } from '@mui/material';
|
||||||
|
import SkeletonDataCard from 'ui-component/cards/Skeleton/DataCard';
|
||||||
|
|
||||||
|
export default function DataCard({ isLoading, title, content, tip, subContent }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isLoading ? (
|
||||||
|
<SkeletonDataCard />
|
||||||
|
) : (
|
||||||
|
<SubCard sx={{ height: '160px' }}>
|
||||||
|
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h3" sx={{ fontSize: '2rem', lineHeight: 1.5, fontWeight: 700 }}>
|
||||||
|
{tip ? (
|
||||||
|
<Tooltip title={tip} placement="top">
|
||||||
|
<span>{content}</span>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
content
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
<Divider />
|
||||||
|
<Typography variant="subtitle2" sx={{ mt: 2 }}>
|
||||||
|
{subContent}
|
||||||
|
</Typography>
|
||||||
|
</SubCard>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DataCard.propTypes = {
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
title: PropTypes.string,
|
||||||
|
content: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||||
|
tip: PropTypes.node,
|
||||||
|
subContent: PropTypes.node
|
||||||
|
};
|
17
web/src/ui-component/cards/Skeleton/DataCard.js
Normal file
17
web/src/ui-component/cards/Skeleton/DataCard.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// material-ui
|
||||||
|
import Skeleton from '@mui/material/Skeleton';
|
||||||
|
import SubCard from 'ui-component/cards/SubCard';
|
||||||
|
import { Divider, Stack } from '@mui/material';
|
||||||
|
|
||||||
|
const DataCard = () => (
|
||||||
|
<SubCard sx={{ height: '160px' }}>
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<Skeleton variant="rectangular" height={20} width={80} />
|
||||||
|
<Skeleton variant="rectangular" height={41} width={50} />
|
||||||
|
<Divider />
|
||||||
|
<Skeleton variant="rectangular" />
|
||||||
|
</Stack>
|
||||||
|
</SubCard>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default DataCard;
|
63
web/src/ui-component/chart/ApexCharts.js
Normal file
63
web/src/ui-component/chart/ApexCharts.js
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
// material-ui
|
||||||
|
import { Grid, Typography } from '@mui/material';
|
||||||
|
|
||||||
|
// third-party
|
||||||
|
import Chart from 'react-apexcharts';
|
||||||
|
|
||||||
|
// project imports
|
||||||
|
import SkeletonTotalGrowthBarChart from 'ui-component/cards/Skeleton/TotalGrowthBarChart';
|
||||||
|
import MainCard from 'ui-component/cards/MainCard';
|
||||||
|
import { gridSpacing } from 'store/constant';
|
||||||
|
import { Box } from '@mui/material';
|
||||||
|
|
||||||
|
// ==============================|| DASHBOARD DEFAULT - TOTAL GROWTH BAR CHART ||============================== //
|
||||||
|
|
||||||
|
const ApexCharts = ({ isLoading, chartDatas, title = '统计' }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isLoading ? (
|
||||||
|
<SkeletonTotalGrowthBarChart />
|
||||||
|
) : (
|
||||||
|
<MainCard>
|
||||||
|
<Grid container spacing={gridSpacing}>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Grid container alignItems="center" justifyContent="space-between">
|
||||||
|
<Grid item>
|
||||||
|
<Typography variant="h3">{title}</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
{chartDatas.series ? (
|
||||||
|
<Chart {...chartDatas} />
|
||||||
|
) : (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
minHeight: '490px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h3" color={'#697586'}>
|
||||||
|
暂无数据
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</MainCard>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ApexCharts.propTypes = {
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
chartDatas: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||||
|
title: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ApexCharts;
|
@ -18,7 +18,7 @@ export function getTodayDay() {
|
|||||||
return today.toISOString().slice(0, 10);
|
return today.toISOString().slice(0, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateChartOptions(data, unit) {
|
export function generateLineChartOptions(data, unit) {
|
||||||
const dates = data.map((item) => item.date);
|
const dates = data.map((item) => item.date);
|
||||||
const values = data.map((item) => item.value);
|
const values = data.map((item) => item.value);
|
||||||
|
|
||||||
@ -94,3 +94,146 @@ export function generateChartOptions(data, unit) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function generateBarChartOptions(xaxis, data, unit = '', decimal = 0) {
|
||||||
|
return {
|
||||||
|
height: 480,
|
||||||
|
type: 'bar',
|
||||||
|
options: {
|
||||||
|
title: {
|
||||||
|
align: 'left',
|
||||||
|
style: {
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontFamily: 'Roboto, sans-serif'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors: [
|
||||||
|
'#008FFB',
|
||||||
|
'#00E396',
|
||||||
|
'#FEB019',
|
||||||
|
'#FF4560',
|
||||||
|
'#775DD0',
|
||||||
|
'#55efc4',
|
||||||
|
'#81ecec',
|
||||||
|
'#74b9ff',
|
||||||
|
'#a29bfe',
|
||||||
|
'#00b894',
|
||||||
|
'#00cec9',
|
||||||
|
'#0984e3',
|
||||||
|
'#6c5ce7',
|
||||||
|
'#ffeaa7',
|
||||||
|
'#fab1a0',
|
||||||
|
'#ff7675',
|
||||||
|
'#fd79a8',
|
||||||
|
'#fdcb6e',
|
||||||
|
'#e17055',
|
||||||
|
'#d63031',
|
||||||
|
'#e84393'
|
||||||
|
],
|
||||||
|
chart: {
|
||||||
|
id: 'bar-chart',
|
||||||
|
stacked: true,
|
||||||
|
toolbar: {
|
||||||
|
show: true
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responsive: [
|
||||||
|
{
|
||||||
|
breakpoint: 480,
|
||||||
|
options: {
|
||||||
|
legend: {
|
||||||
|
position: 'bottom',
|
||||||
|
offsetX: -10,
|
||||||
|
offsetY: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
plotOptions: {
|
||||||
|
bar: {
|
||||||
|
horizontal: false,
|
||||||
|
columnWidth: '50%',
|
||||||
|
// borderRadius: 10,
|
||||||
|
dataLabels: {
|
||||||
|
total: {
|
||||||
|
enabled: true,
|
||||||
|
style: {
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: 900
|
||||||
|
},
|
||||||
|
formatter: function (val) {
|
||||||
|
return renderChartNumber(val, decimal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
type: 'category',
|
||||||
|
categories: xaxis
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
show: true,
|
||||||
|
fontSize: '14px',
|
||||||
|
fontFamily: `'Roboto', sans-serif`,
|
||||||
|
position: 'bottom',
|
||||||
|
offsetX: 20,
|
||||||
|
labels: {
|
||||||
|
useSeriesColors: false
|
||||||
|
},
|
||||||
|
markers: {
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
radius: 5
|
||||||
|
},
|
||||||
|
itemMargin: {
|
||||||
|
horizontal: 15,
|
||||||
|
vertical: 8
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fill: {
|
||||||
|
type: 'solid'
|
||||||
|
},
|
||||||
|
dataLabels: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
show: true
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
theme: 'dark',
|
||||||
|
fixed: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
marker: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
formatter: function (val) {
|
||||||
|
return renderChartNumber(val, decimal) + ` ${unit}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化数值
|
||||||
|
export function renderChartNumber(number, decimal = 2) {
|
||||||
|
number = number.toFixed(decimal);
|
||||||
|
if (number === Number(0).toFixed(decimal)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果大于1000,显示为k
|
||||||
|
if (number >= 1000) {
|
||||||
|
return (number / 1000).toFixed(1) + 'k';
|
||||||
|
}
|
||||||
|
|
||||||
|
return number;
|
||||||
|
}
|
||||||
|
@ -14,7 +14,7 @@ import { Box } from '@mui/material';
|
|||||||
|
|
||||||
// ==============================|| DASHBOARD DEFAULT - TOTAL GROWTH BAR CHART ||============================== //
|
// ==============================|| DASHBOARD DEFAULT - TOTAL GROWTH BAR CHART ||============================== //
|
||||||
|
|
||||||
const StatisticalBarChart = ({ isLoading, chartDatas }) => {
|
const BubbleChard = ({ isLoading, chartDatas, title = '统计' }) => {
|
||||||
chartData.options.xaxis.categories = chartDatas.xaxis;
|
chartData.options.xaxis.categories = chartDatas.xaxis;
|
||||||
chartData.series = chartDatas.data;
|
chartData.series = chartDatas.data;
|
||||||
|
|
||||||
@ -28,7 +28,7 @@ const StatisticalBarChart = ({ isLoading, chartDatas }) => {
|
|||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Grid container alignItems="center" justifyContent="space-between">
|
<Grid container alignItems="center" justifyContent="space-between">
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography variant="h3">统计</Typography>
|
<Typography variant="h3">{title}</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -57,16 +57,17 @@ const StatisticalBarChart = ({ isLoading, chartDatas }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
StatisticalBarChart.propTypes = {
|
BubbleChard.propTypes = {
|
||||||
isLoading: PropTypes.bool,
|
isLoading: PropTypes.bool,
|
||||||
chartDatas: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
|
chartDatas: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||||
|
title: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
export default StatisticalBarChart;
|
export default BubbleChard;
|
||||||
|
|
||||||
const chartData = {
|
const chartData = {
|
||||||
height: 480,
|
height: 480,
|
||||||
type: 'bar',
|
type: 'bubble',
|
||||||
options: {
|
options: {
|
||||||
colors: [
|
colors: [
|
||||||
'#008FFB',
|
'#008FFB',
|
||||||
@ -92,7 +93,7 @@ const chartData = {
|
|||||||
'#e84393'
|
'#e84393'
|
||||||
],
|
],
|
||||||
chart: {
|
chart: {
|
||||||
id: 'bar-chart',
|
id: 'bubble',
|
||||||
stacked: true,
|
stacked: true,
|
||||||
toolbar: {
|
toolbar: {
|
||||||
show: true
|
show: true
|
||||||
@ -156,11 +157,6 @@ const chartData = {
|
|||||||
fixed: {
|
fixed: {
|
||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
y: {
|
|
||||||
formatter: function (val) {
|
|
||||||
return '$' + val;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
marker: {
|
marker: {
|
||||||
show: false
|
show: false
|
||||||
}
|
}
|
334
web/src/views/Analytics/component/Overview.js
Normal file
334
web/src/views/Analytics/component/Overview.js
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { Grid, Typography, Divider } from '@mui/material';
|
||||||
|
import { gridSpacing } from 'store/constant';
|
||||||
|
import DateRangePicker from 'ui-component/DateRangePicker';
|
||||||
|
import ApexCharts from 'ui-component/chart/ApexCharts';
|
||||||
|
import { showError, calculateQuota } from 'utils/common';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { API } from 'utils/api';
|
||||||
|
import { generateBarChartOptions, renderChartNumber } from 'utils/chart';
|
||||||
|
|
||||||
|
export default function Overview() {
|
||||||
|
const [channelLoading, setChannelLoading] = useState(true);
|
||||||
|
const [redemptionLoading, setRedemptionLoading] = useState(true);
|
||||||
|
const [usersLoading, setUsersLoading] = useState(true);
|
||||||
|
const [channelData, setChannelData] = useState([]);
|
||||||
|
const [redemptionData, setRedemptionData] = useState([]);
|
||||||
|
const [usersData, setUsersData] = useState([]);
|
||||||
|
const [dateRange, setDateRange] = useState({ start: dayjs().subtract(6, 'day').startOf('day'), end: dayjs().endOf('day') });
|
||||||
|
const handleDateRangeChange = (value) => {
|
||||||
|
setDateRange(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const channelChart = useCallback(async () => {
|
||||||
|
setChannelLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await API.get('/api/analytics/channel_period', {
|
||||||
|
params: {
|
||||||
|
start_timestamp: dateRange.start.unix(),
|
||||||
|
end_timestamp: dateRange.end.unix()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
if (data) {
|
||||||
|
setChannelData(getBarChartOptions(data, dateRange));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
setChannelLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, [dateRange]);
|
||||||
|
|
||||||
|
const redemptionChart = useCallback(async () => {
|
||||||
|
setRedemptionLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await API.get('/api/analytics/redemption_period', {
|
||||||
|
params: {
|
||||||
|
start_timestamp: dateRange.start.unix(),
|
||||||
|
end_timestamp: dateRange.end.unix()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
if (data) {
|
||||||
|
let chartData = getRedemptionData(data, dateRange);
|
||||||
|
setRedemptionData(chartData);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
setRedemptionLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, [dateRange]);
|
||||||
|
|
||||||
|
const usersChart = useCallback(async () => {
|
||||||
|
setUsersLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await API.get('/api/analytics/users_period', {
|
||||||
|
params: {
|
||||||
|
start_timestamp: dateRange.start.unix(),
|
||||||
|
end_timestamp: dateRange.end.unix()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
if (data) {
|
||||||
|
setUsersData(getUsersData(data, dateRange));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
setUsersLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, [dateRange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
channelChart();
|
||||||
|
redemptionChart();
|
||||||
|
usersChart();
|
||||||
|
}, [dateRange, channelChart, redemptionChart, usersChart]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container spacing={gridSpacing}>
|
||||||
|
<Grid item lg={8} xs={12}>
|
||||||
|
<DateRangePicker defaultValue={dateRange} onChange={handleDateRangeChange} localeText={{ start: '开始时间', end: '结束时间' }} />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Typography variant="h3">
|
||||||
|
{dateRange.start.format('YYYY-MM-DD')} - {dateRange.end.format('YYYY-MM-DD')}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Divider />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<ApexCharts id="cost" isLoading={channelLoading} chartDatas={channelData?.costs || {}} title="消费统计" decimal={3} />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<ApexCharts id="token" isLoading={channelLoading} chartDatas={channelData?.tokens || {}} title="Tokens统计" unit="" />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<ApexCharts id="latency" isLoading={channelLoading} chartDatas={channelData?.latency || {}} title="平均延迟" unit="" />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<ApexCharts id="requests" isLoading={channelLoading} chartDatas={channelData?.requests || {}} title="请求数" unit="" />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<ApexCharts isLoading={redemptionLoading} chartDatas={redemptionData} title="兑换统计" />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<ApexCharts isLoading={usersLoading} chartDatas={usersData} title="注册统计" />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDates(start, end) {
|
||||||
|
var dates = [];
|
||||||
|
var current = start;
|
||||||
|
|
||||||
|
while (current.isBefore(end) || current.isSame(end)) {
|
||||||
|
dates.push(current.format('YYYY-MM-DD'));
|
||||||
|
current = current.add(1, 'day');
|
||||||
|
}
|
||||||
|
|
||||||
|
return dates;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateDailyData(item, dateMap) {
|
||||||
|
const index = dateMap.get(item.Date);
|
||||||
|
if (index === undefined) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: item.Channel,
|
||||||
|
costs: calculateQuota(item.Quota, 3),
|
||||||
|
tokens: item.PromptTokens + item.CompletionTokens,
|
||||||
|
requests: item.RequestCount,
|
||||||
|
latency: Number(item.RequestTime / 1000 / item.RequestCount).toFixed(3),
|
||||||
|
index: index
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBarDataGroup(data, dates) {
|
||||||
|
const dateMap = new Map(dates.map((date, index) => [date, index]));
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
costs: { total: 0, data: new Map() },
|
||||||
|
tokens: { total: 0, data: new Map() },
|
||||||
|
requests: { total: 0, data: new Map() },
|
||||||
|
latency: { total: 0, data: new Map() }
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const item of data) {
|
||||||
|
const dailyData = calculateDailyData(item, dateMap);
|
||||||
|
if (!dailyData) continue;
|
||||||
|
|
||||||
|
for (let key in result) {
|
||||||
|
if (!result[key].data.has(dailyData.name)) {
|
||||||
|
result[key].data.set(dailyData.name, { name: dailyData.name, data: new Array(dates.length).fill(0) });
|
||||||
|
}
|
||||||
|
const channelDailyData = result[key].data.get(dailyData.name);
|
||||||
|
channelDailyData.data[dailyData.index] = dailyData[key];
|
||||||
|
result[key].total += Number(dailyData[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBarChartOptions(data, dateRange) {
|
||||||
|
const dates = getDates(dateRange.start, dateRange.end);
|
||||||
|
const result = getBarDataGroup(data, dates);
|
||||||
|
|
||||||
|
let channelData = {};
|
||||||
|
|
||||||
|
channelData.costs = generateBarChartOptions(dates, Array.from(result.costs.data.values()), '美元', 3);
|
||||||
|
channelData.costs.options.title.text = '总消费:$' + renderChartNumber(result.costs.total, 3);
|
||||||
|
|
||||||
|
channelData.tokens = generateBarChartOptions(dates, Array.from(result.tokens.data.values()), '', 0);
|
||||||
|
channelData.tokens.options.title.text = '总Tokens:' + renderChartNumber(result.tokens.total, 0);
|
||||||
|
|
||||||
|
channelData.requests = generateBarChartOptions(dates, Array.from(result.requests.data.values()), '次', 0);
|
||||||
|
channelData.requests.options.title.text = '总请求数:' + renderChartNumber(result.requests.total, 0);
|
||||||
|
|
||||||
|
// 获取每天所有渠道的平均延迟
|
||||||
|
let latency = Array.from(result.latency.data.values());
|
||||||
|
let sums = [];
|
||||||
|
let counts = [];
|
||||||
|
for (let obj of latency) {
|
||||||
|
for (let i = 0; i < obj.data.length; i++) {
|
||||||
|
let value = parseFloat(obj.data[i]);
|
||||||
|
sums[i] = sums[i] || 0;
|
||||||
|
counts[i] = counts[i] || 0;
|
||||||
|
if (value !== 0) {
|
||||||
|
sums[i] = (sums[i] || 0) + value;
|
||||||
|
counts[i] = (counts[i] || 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 追加latency列表后面
|
||||||
|
latency[latency.length] = {
|
||||||
|
name: '平均延迟',
|
||||||
|
data: sums.map((sum, i) => Number(counts[i] ? sum / counts[i] : 0).toFixed(3))
|
||||||
|
};
|
||||||
|
|
||||||
|
let dashArray = new Array(latency.length - 1).fill(0);
|
||||||
|
dashArray.push(5);
|
||||||
|
|
||||||
|
channelData.latency = generateBarChartOptions(dates, latency, '秒', 3);
|
||||||
|
channelData.latency.type = 'line';
|
||||||
|
channelData.latency.options.chart = {
|
||||||
|
type: 'line',
|
||||||
|
zoom: {
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
channelData.latency.options.stroke = {
|
||||||
|
curve: 'smooth',
|
||||||
|
dashArray: dashArray
|
||||||
|
};
|
||||||
|
|
||||||
|
return channelData;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRedemptionData(data, dateRange) {
|
||||||
|
const dates = getDates(dateRange.start, dateRange.end);
|
||||||
|
const result = [
|
||||||
|
{
|
||||||
|
name: '兑换金额($)',
|
||||||
|
type: 'column',
|
||||||
|
data: new Array(dates.length).fill(0)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '独立用户(人)',
|
||||||
|
type: 'line',
|
||||||
|
data: new Array(dates.length).fill(0)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const item of data) {
|
||||||
|
const index = dates.indexOf(item.date);
|
||||||
|
if (index !== -1) {
|
||||||
|
result[0].data[index] = calculateQuota(item.quota, 3);
|
||||||
|
result[1].data[index] = item.user_count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let chartData = {
|
||||||
|
height: 480,
|
||||||
|
options: {
|
||||||
|
chart: {
|
||||||
|
type: 'line'
|
||||||
|
},
|
||||||
|
stroke: {
|
||||||
|
width: [0, 4]
|
||||||
|
},
|
||||||
|
dataLabels: {
|
||||||
|
enabled: true,
|
||||||
|
enabledOnSeries: [1]
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
type: 'category',
|
||||||
|
categories: dates
|
||||||
|
},
|
||||||
|
yaxis: [
|
||||||
|
{
|
||||||
|
title: {
|
||||||
|
text: '兑换金额($)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
opposite: true,
|
||||||
|
title: {
|
||||||
|
text: '独立用户(人)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
tooltip: {
|
||||||
|
theme: 'dark'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: result
|
||||||
|
};
|
||||||
|
|
||||||
|
return chartData;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUsersData(data, dateRange) {
|
||||||
|
const dates = getDates(dateRange.start, dateRange.end);
|
||||||
|
const result = [
|
||||||
|
{
|
||||||
|
name: '直接注册',
|
||||||
|
data: new Array(dates.length).fill(0)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '邀请注册',
|
||||||
|
data: new Array(dates.length).fill(0)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let total = 0;
|
||||||
|
|
||||||
|
for (const item of data) {
|
||||||
|
const index = dates.indexOf(item.date);
|
||||||
|
if (index !== -1) {
|
||||||
|
result[0].data[index] = item.user_count - item.inviter_user_count;
|
||||||
|
result[1].data[index] = item.inviter_user_count;
|
||||||
|
|
||||||
|
total += item.user_count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let chartData = generateBarChartOptions(dates, result, '人', 0);
|
||||||
|
chartData.options.title.text = '总注册人数:' + total;
|
||||||
|
|
||||||
|
return chartData;
|
||||||
|
}
|
151
web/src/views/Analytics/component/Statistics.js
Normal file
151
web/src/views/Analytics/component/Statistics.js
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { Grid } from '@mui/material';
|
||||||
|
import DataCard from 'ui-component/cards/DataCard';
|
||||||
|
import { gridSpacing } from 'store/constant';
|
||||||
|
import { showError, renderQuota } from 'utils/common';
|
||||||
|
import { API } from 'utils/api';
|
||||||
|
|
||||||
|
export default function Overview() {
|
||||||
|
const [userLoading, setUserLoading] = useState(true);
|
||||||
|
const [channelLoading, setChannelLoading] = useState(true);
|
||||||
|
const [redemptionLoading, setRedemptionLoading] = useState(true);
|
||||||
|
const [userStatistics, setUserStatistics] = useState({});
|
||||||
|
|
||||||
|
const [channelStatistics, setChannelStatistics] = useState({
|
||||||
|
active: 0,
|
||||||
|
disabled: 0,
|
||||||
|
test_disabled: 0,
|
||||||
|
total: 0
|
||||||
|
});
|
||||||
|
const [redemptionStatistics, setRedemptionStatistics] = useState({
|
||||||
|
total: 0,
|
||||||
|
used: 0,
|
||||||
|
unused: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const userStatisticsData = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const res = await API.get('/api/analytics/user_statistics');
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
data.total_quota = renderQuota(data.total_quota);
|
||||||
|
data.total_used_quota = renderQuota(data.total_used_quota);
|
||||||
|
data.total_direct_user = data.total_user - data.total_inviter_user;
|
||||||
|
setUserStatistics(data);
|
||||||
|
setUserLoading(false);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const channelStatisticsData = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const res = await API.get('/api/analytics/channel_statistics');
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
let channelData = channelStatistics;
|
||||||
|
channelData.total = 0;
|
||||||
|
data.forEach((item) => {
|
||||||
|
if (item.status === 1) {
|
||||||
|
channelData.active = item.total_channels;
|
||||||
|
} else if (item.status === 2) {
|
||||||
|
channelData.disabled = item.total_channels;
|
||||||
|
} else if (item.status === 3) {
|
||||||
|
channelData.test_disabled = item.total_channels;
|
||||||
|
}
|
||||||
|
channelData.total += item.total_channels;
|
||||||
|
});
|
||||||
|
setChannelStatistics(channelData);
|
||||||
|
setChannelLoading(false);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, [channelStatistics]);
|
||||||
|
|
||||||
|
const redemptionStatisticsData = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const res = await API.get('/api/analytics/redemption_statistics');
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
let redemptionData = redemptionStatistics;
|
||||||
|
redemptionData.total = 0;
|
||||||
|
data.forEach((item) => {
|
||||||
|
if (item.status === 1) {
|
||||||
|
redemptionData.unused = renderQuota(item.quota);
|
||||||
|
} else if (item.status === 3) {
|
||||||
|
redemptionData.used = renderQuota(item.quota);
|
||||||
|
}
|
||||||
|
redemptionData.total += item.quota;
|
||||||
|
});
|
||||||
|
redemptionData.total = renderQuota(redemptionData.total);
|
||||||
|
setRedemptionStatistics(redemptionData);
|
||||||
|
setRedemptionLoading(false);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, [redemptionStatistics]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
userStatisticsData();
|
||||||
|
channelStatisticsData();
|
||||||
|
redemptionStatisticsData();
|
||||||
|
}, [userStatisticsData, channelStatisticsData, redemptionStatisticsData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container spacing={gridSpacing}>
|
||||||
|
<Grid item lg={3} xs={12}>
|
||||||
|
<DataCard
|
||||||
|
isLoading={userLoading}
|
||||||
|
title="用户总消费金额"
|
||||||
|
content={userStatistics?.total_used_quota || '0'}
|
||||||
|
subContent={'用户总余额:' + (userStatistics?.total_quota || '0')}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item lg={3} xs={12}>
|
||||||
|
<DataCard
|
||||||
|
isLoading={userLoading}
|
||||||
|
title="用户总数"
|
||||||
|
content={userStatistics?.total_user || '0'}
|
||||||
|
subContent={
|
||||||
|
<>
|
||||||
|
直接注册:{userStatistics?.total_direct_user || '0'} <br /> 邀请注册:{userStatistics?.total_inviter_user || '0'}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item lg={3} xs={12}>
|
||||||
|
<DataCard
|
||||||
|
isLoading={channelLoading}
|
||||||
|
title="渠道数量"
|
||||||
|
content={channelStatistics.total}
|
||||||
|
subContent={
|
||||||
|
<>
|
||||||
|
正常:{channelStatistics.active} / 禁用:{channelStatistics.disabled} / 测试禁用:{channelStatistics.test_disabled}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item lg={3} xs={12}>
|
||||||
|
<DataCard
|
||||||
|
isLoading={redemptionLoading}
|
||||||
|
title="兑换码发行量"
|
||||||
|
content={redemptionStatistics.total}
|
||||||
|
subContent={
|
||||||
|
<>
|
||||||
|
已使用: {redemptionStatistics.used} <br /> 未使用: {redemptionStatistics.unused}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
20
web/src/views/Analytics/index.js
Normal file
20
web/src/views/Analytics/index.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { gridSpacing } from 'store/constant';
|
||||||
|
import { Grid } from '@mui/material';
|
||||||
|
import MainCard from 'ui-component/cards/MainCard';
|
||||||
|
import Statistics from './component/Statistics';
|
||||||
|
import Overview from './component/Overview';
|
||||||
|
|
||||||
|
export default function MarketingData() {
|
||||||
|
return (
|
||||||
|
<Grid container spacing={gridSpacing}>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Statistics />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<MainCard>
|
||||||
|
<Overview />
|
||||||
|
</MainCard>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
@ -2,9 +2,9 @@ import { useEffect, useState } from 'react';
|
|||||||
import { Grid, Typography } from '@mui/material';
|
import { Grid, Typography } from '@mui/material';
|
||||||
import { gridSpacing } from 'store/constant';
|
import { gridSpacing } from 'store/constant';
|
||||||
import StatisticalLineChartCard from './component/StatisticalLineChartCard';
|
import StatisticalLineChartCard from './component/StatisticalLineChartCard';
|
||||||
import StatisticalBarChart from './component/StatisticalBarChart';
|
import ApexCharts from 'ui-component/chart/ApexCharts';
|
||||||
import SupportModels from './component/SupportModels';
|
import SupportModels from './component/SupportModels';
|
||||||
import { generateChartOptions, getLastSevenDays } from 'utils/chart';
|
import { generateLineChartOptions, getLastSevenDays, generateBarChartOptions, renderChartNumber } from 'utils/chart';
|
||||||
import { API } from 'utils/api';
|
import { API } from 'utils/api';
|
||||||
import { showError, calculateQuota, renderNumber } from 'utils/common';
|
import { showError, calculateQuota, renderNumber } from 'utils/common';
|
||||||
import UserCard from 'ui-component/cards/UserCard';
|
import UserCard from 'ui-component/cards/UserCard';
|
||||||
@ -94,7 +94,7 @@ const Dashboard = () => {
|
|||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Grid container spacing={gridSpacing}>
|
<Grid container spacing={gridSpacing}>
|
||||||
<Grid item lg={8} xs={12}>
|
<Grid item lg={8} xs={12}>
|
||||||
<StatisticalBarChart isLoading={isLoading} chartDatas={statisticalData} />
|
<ApexCharts isLoading={isLoading} chartDatas={statisticalData} />
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item lg={4} xs={12}>
|
<Grid item lg={4} xs={12}>
|
||||||
<UserCard>
|
<UserCard>
|
||||||
@ -129,33 +129,33 @@ export default Dashboard;
|
|||||||
|
|
||||||
function getLineDataGroup(statisticalData) {
|
function getLineDataGroup(statisticalData) {
|
||||||
let groupedData = statisticalData.reduce((acc, cur) => {
|
let groupedData = statisticalData.reduce((acc, cur) => {
|
||||||
if (!acc[cur.Day]) {
|
if (!acc[cur.Date]) {
|
||||||
acc[cur.Day] = {
|
acc[cur.Date] = {
|
||||||
date: cur.Day,
|
date: cur.Date,
|
||||||
RequestCount: 0,
|
RequestCount: 0,
|
||||||
Quota: 0,
|
Quota: 0,
|
||||||
PromptTokens: 0,
|
PromptTokens: 0,
|
||||||
CompletionTokens: 0
|
CompletionTokens: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
acc[cur.Day].RequestCount += cur.RequestCount;
|
acc[cur.Date].RequestCount += cur.RequestCount;
|
||||||
acc[cur.Day].Quota += cur.Quota;
|
acc[cur.Date].Quota += cur.Quota;
|
||||||
acc[cur.Day].PromptTokens += cur.PromptTokens;
|
acc[cur.Date].PromptTokens += cur.PromptTokens;
|
||||||
acc[cur.Day].CompletionTokens += cur.CompletionTokens;
|
acc[cur.Date].CompletionTokens += cur.CompletionTokens;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
let lastSevenDays = getLastSevenDays();
|
let lastSevenDays = getLastSevenDays();
|
||||||
return lastSevenDays.map((day) => {
|
return lastSevenDays.map((Date) => {
|
||||||
if (!groupedData[day]) {
|
if (!groupedData[Date]) {
|
||||||
return {
|
return {
|
||||||
date: day,
|
date: Date,
|
||||||
RequestCount: 0,
|
RequestCount: 0,
|
||||||
Quota: 0,
|
Quota: 0,
|
||||||
PromptTokens: 0,
|
PromptTokens: 0,
|
||||||
CompletionTokens: 0
|
CompletionTokens: 0
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return groupedData[day];
|
return groupedData[Date];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -164,28 +164,26 @@ function getBarDataGroup(data) {
|
|||||||
const lastSevenDays = getLastSevenDays();
|
const lastSevenDays = getLastSevenDays();
|
||||||
const result = [];
|
const result = [];
|
||||||
const map = new Map();
|
const map = new Map();
|
||||||
|
let totalCosts = 0;
|
||||||
|
|
||||||
for (const item of data) {
|
for (const item of data) {
|
||||||
if (!map.has(item.ModelName)) {
|
if (!map.has(item.ModelName)) {
|
||||||
const newData = { name: item.ModelName, data: new Array(7) };
|
const newData = { name: item.ModelName, data: new Array(7).fill(0) };
|
||||||
map.set(item.ModelName, newData);
|
map.set(item.ModelName, newData);
|
||||||
result.push(newData);
|
result.push(newData);
|
||||||
}
|
}
|
||||||
const index = lastSevenDays.indexOf(item.Day);
|
const index = lastSevenDays.indexOf(item.Date);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
map.get(item.ModelName).data[index] = calculateQuota(item.Quota, 3);
|
let costs = Number(calculateQuota(item.Quota, 3));
|
||||||
|
map.get(item.ModelName).data[index] = costs;
|
||||||
|
totalCosts += parseFloat(costs.toFixed(3));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const item of result) {
|
let chartData = generateBarChartOptions(lastSevenDays, result, '美元', 3);
|
||||||
for (let i = 0; i < 7; i++) {
|
chartData.options.title.text = '7日总消费:$' + renderChartNumber(totalCosts, 3);
|
||||||
if (item.data[i] === undefined) {
|
|
||||||
item.data[i] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { data: result, xaxis: lastSevenDays };
|
return chartData;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLineCardOption(lineDataGroup, field) {
|
function getLineCardOption(lineDataGroup, field) {
|
||||||
@ -214,15 +212,15 @@ function getLineCardOption(lineDataGroup, field) {
|
|||||||
|
|
||||||
switch (field) {
|
switch (field) {
|
||||||
case 'RequestCount':
|
case 'RequestCount':
|
||||||
chartData = generateChartOptions(lineData, '次');
|
chartData = generateLineChartOptions(lineData, '次');
|
||||||
todayValue = renderNumber(todayValue);
|
todayValue = renderNumber(todayValue);
|
||||||
break;
|
break;
|
||||||
case 'Quota':
|
case 'Quota':
|
||||||
chartData = generateChartOptions(lineData, '美元');
|
chartData = generateLineChartOptions(lineData, '美元');
|
||||||
todayValue = '$' + renderNumber(todayValue);
|
todayValue = '$' + renderNumber(todayValue);
|
||||||
break;
|
break;
|
||||||
case 'PromptTokens':
|
case 'PromptTokens':
|
||||||
chartData = generateChartOptions(lineData, '');
|
chartData = generateLineChartOptions(lineData, '');
|
||||||
todayValue = renderNumber(todayValue);
|
todayValue = renderNumber(todayValue);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user