diff --git a/common/constants.go b/common/constants.go index c4d2511ef357..b62e96ffafd9 100644 --- a/common/constants.go +++ b/common/constants.go @@ -47,6 +47,7 @@ var TaskEnabled = true var DataExportEnabled = true var DataExportInterval = 5 // unit: minute var DataExportDefaultTime = "hour" // unit: minute +var DataDashboardChartTabs = "" // comma-separated visible tab keys, empty = all var DefaultCollapseSidebar = false // default value of collapse sidebar // Any options with "Secret", "Token" in its key won't be return by GetOptions diff --git a/controller/misc.go b/controller/misc.go index 29b3a5c5e180..0c5ef0c7ba4c 100644 --- a/controller/misc.go +++ b/controller/misc.go @@ -82,6 +82,7 @@ func GetStatus(c *gin.Context) { "enable_task": common.TaskEnabled, "enable_data_export": common.DataExportEnabled, "data_export_default_time": common.DataExportDefaultTime, + "data_dashboard_chart_tabs": common.DataDashboardChartTabs, "default_collapse_sidebar": common.DefaultCollapseSidebar, "mj_notify_enabled": setting.MjNotifyEnabled, "chats": setting.Chats, diff --git a/controller/usedata.go b/controller/usedata.go index 5e194c517506..6b4ba3af0dc1 100644 --- a/controller/usedata.go +++ b/controller/usedata.go @@ -14,7 +14,8 @@ func GetAllQuotaDates(c *gin.Context) { startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64) endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64) username := c.Query("username") - dates, err := model.GetAllQuotaDates(startTimestamp, endTimestamp, username) + modelName := c.Query("model_name") + dates, err := model.GetAllQuotaDates(startTimestamp, endTimestamp, username, modelName) if err != nil { common.ApiError(c, err) return @@ -30,7 +31,8 @@ func GetAllQuotaDates(c *gin.Context) { func GetQuotaDatesByUser(c *gin.Context) { startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64) endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64) - dates, err := model.GetQuotaDataGroupByUser(startTimestamp, endTimestamp) + modelName := c.Query("model_name") + dates, err := model.GetQuotaDataGroupByUser(startTimestamp, endTimestamp, modelName) if err != nil { common.ApiError(c, err) return @@ -54,7 +56,8 @@ func GetUserQuotaDates(c *gin.Context) { }) return } - dates, err := model.GetQuotaDataByUserId(userId, startTimestamp, endTimestamp) + modelName := c.Query("model_name") + dates, err := model.GetQuotaDataByUserId(userId, startTimestamp, endTimestamp, modelName) if err != nil { common.ApiError(c, err) return diff --git a/model/option.go b/model/option.go index e0a3048d34f2..98aa0e15b2f6 100644 --- a/model/option.go +++ b/model/option.go @@ -159,6 +159,7 @@ func InitOptionMap() { common.OptionMap["RetryTimes"] = strconv.Itoa(common.RetryTimes) common.OptionMap["DataExportInterval"] = strconv.Itoa(common.DataExportInterval) common.OptionMap["DataExportDefaultTime"] = common.DataExportDefaultTime + common.OptionMap["DataDashboardChartTabs"] = common.DataDashboardChartTabs common.OptionMap["DefaultCollapseSidebar"] = strconv.FormatBool(common.DefaultCollapseSidebar) common.OptionMap["MjNotifyEnabled"] = strconv.FormatBool(setting.MjNotifyEnabled) common.OptionMap["MjAccountFilterEnabled"] = strconv.FormatBool(setting.MjAccountFilterEnabled) @@ -499,6 +500,8 @@ func updateOptionMap(key string, value string) (err error) { common.DataExportInterval, _ = strconv.Atoi(value) case "DataExportDefaultTime": common.DataExportDefaultTime = value + case "DataDashboardChartTabs": + common.DataDashboardChartTabs = value case "ModelRatio": err = ratio_setting.UpdateModelRatioByJSONString(value) case "GroupRatio": diff --git a/model/usedata.go b/model/usedata.go index f0ea055ae395..e365557b9741 100644 --- a/model/usedata.go +++ b/model/usedata.go @@ -101,38 +101,52 @@ func increaseQuotaData(userId int, username string, modelName string, count int, } } -func GetQuotaDataByUsername(username string, startTime int64, endTime int64) (quotaData []*QuotaData, err error) { +func GetQuotaDataByUsername(username string, startTime int64, endTime int64, modelName string) (quotaData []*QuotaData, err error) { var quotaDatas []*QuotaData // 从quota_data表中查询数据 - err = DB.Table("quota_data").Where("username = ? and created_at >= ? and created_at <= ?", username, startTime, endTime).Find("aDatas).Error + tx := DB.Table("quota_data").Where("username = ? and created_at >= ? and created_at <= ?", username, startTime, endTime) + if modelName != "" { + tx = tx.Where("model_name = ?", modelName) + } + err = tx.Find("aDatas).Error return quotaDatas, err } -func GetQuotaDataByUserId(userId int, startTime int64, endTime int64) (quotaData []*QuotaData, err error) { +func GetQuotaDataByUserId(userId int, startTime int64, endTime int64, modelName string) (quotaData []*QuotaData, err error) { var quotaDatas []*QuotaData // 从quota_data表中查询数据 - err = DB.Table("quota_data").Where("user_id = ? and created_at >= ? and created_at <= ?", userId, startTime, endTime).Find("aDatas).Error + tx := DB.Table("quota_data").Where("user_id = ? and created_at >= ? and created_at <= ?", userId, startTime, endTime) + if modelName != "" { + tx = tx.Where("model_name = ?", modelName) + } + err = tx.Find("aDatas).Error return quotaDatas, err } -func GetQuotaDataGroupByUser(startTime int64, endTime int64) (quotaData []*QuotaData, err error) { +func GetQuotaDataGroupByUser(startTime int64, endTime int64, modelName string) (quotaData []*QuotaData, err error) { var quotaDatas []*QuotaData - err = DB.Table("quota_data"). + tx := DB.Table("quota_data"). Select("username, created_at, sum(count) as count, sum(quota) as quota, sum(token_used) as token_used"). - Where("created_at >= ? and created_at <= ?", startTime, endTime). - Group("username, created_at"). + Where("created_at >= ? and created_at <= ?", startTime, endTime) + if modelName != "" { + tx = tx.Where("model_name = ?", modelName) + } + err = tx.Group("username, created_at"). Find("aDatas).Error return quotaDatas, err } -func GetAllQuotaDates(startTime int64, endTime int64, username string) (quotaData []*QuotaData, err error) { +func GetAllQuotaDates(startTime int64, endTime int64, username string, modelName string) (quotaData []*QuotaData, err error) { if username != "" { - return GetQuotaDataByUsername(username, startTime, endTime) + return GetQuotaDataByUsername(username, startTime, endTime, modelName) } var quotaDatas []*QuotaData - // 从quota_data表中查询数据 - // only select model_name, sum(count) as count, sum(quota) as quota, model_name, created_at from quota_data group by model_name, created_at; - //err = DB.Table("quota_data").Where("created_at >= ? and created_at <= ?", startTime, endTime).Find("aDatas).Error - err = DB.Table("quota_data").Select("model_name, sum(count) as count, sum(quota) as quota, sum(token_used) as token_used, created_at").Where("created_at >= ? and created_at <= ?", startTime, endTime).Group("model_name, created_at").Find("aDatas).Error + tx := DB.Table("quota_data"). + Select("model_name, sum(count) as count, sum(quota) as quota, sum(token_used) as token_used, created_at"). + Where("created_at >= ? and created_at <= ?", startTime, endTime) + if modelName != "" { + tx = tx.Where("model_name = ?", modelName) + } + err = tx.Group("model_name, created_at").Find("aDatas).Error return quotaDatas, err } diff --git a/web/classic/src/components/dashboard/ChartsPanel.jsx b/web/classic/src/components/dashboard/ChartsPanel.jsx index 0034ffd90068..a28a7aad435f 100644 --- a/web/classic/src/components/dashboard/ChartsPanel.jsx +++ b/web/classic/src/components/dashboard/ChartsPanel.jsx @@ -17,10 +17,33 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import React from 'react'; -import { Card, Tabs, TabPane } from '@douyinfe/semi-ui'; -import { PieChart } from 'lucide-react'; +import React, { useState, useMemo, useCallback, useEffect } from 'react'; +import { Card, Tabs, TabPane, Popover, Checkbox, CheckboxGroup, Button } from '@douyinfe/semi-ui'; +import { PieChart, Settings } from 'lucide-react'; import { VChart } from '@visactor/react-vchart'; +import { ALL_CHART_TABS, CHART_TABS_NONE, STORAGE_KEYS } from '../../constants/dashboard.constants'; + +// 解析 data_dashboard_chart_tabs 的取值: +// '' → null(未限制,调用方回退到"全部") +// '__none__' → [](管理员显式全部隐藏) +// 'k1,k2' → ['k1', 'k2'] +const parseGlobalChartTabs = (value) => { + if (value === CHART_TABS_NONE) return []; + if (value) return value.split(','); + return null; +}; + +const SPEC_MAP = { + '1': 'spec_line', + '2': 'spec_model_line', + '3': 'spec_pie', + '4': 'spec_rank_bar', + '7': 'spec_token_bar', + '5': 'spec_user_rank', + '6': 'spec_user_trend', + '8': 'spec_user_token_rank', + '9': 'spec_user_token_trend', +}; const ChartsPanel = ({ activeChartTab, @@ -29,8 +52,11 @@ const ChartsPanel = ({ spec_model_line, spec_pie, spec_rank_bar, + spec_token_bar, spec_user_rank, spec_user_trend, + spec_user_token_rank, + spec_user_token_trend, isAdminUser, CARD_PROPS, CHART_CONFIG, @@ -38,6 +64,65 @@ const ChartsPanel = ({ hasApiInfoPanel, t, }) => { + const specs = { + spec_line, + spec_model_line, + spec_pie, + spec_rank_bar, + spec_token_bar, + spec_user_rank, + spec_user_trend, + spec_user_token_rank, + spec_user_token_trend, + }; + + // ========== Tab 可见性逻辑 ========== + const [userTabs, setUserTabs] = useState(() => { + const saved = localStorage.getItem(STORAGE_KEYS.CHART_TABS_USER); + return saved ? saved.split(',') : null; + }); + + const visibleTabs = useMemo(() => { + const globalSetting = localStorage.getItem(STORAGE_KEYS.CHART_TABS_GLOBAL) || ''; + const globalTabs = parseGlobalChartTabs(globalSetting); + const enabledKeys = userTabs || globalTabs || ALL_CHART_TABS.map((tab) => tab.key); + + return ALL_CHART_TABS.filter((tab) => { + if (tab.adminOnly && !isAdminUser) return false; + return enabledKeys.includes(tab.key); + }); + }, [userTabs, isAdminUser]); + + // 如果当前激活的 tab 不在可见列表里,自动切到第一个 + useEffect(() => { + if (visibleTabs.length > 0 && !visibleTabs.find((tab) => tab.key === activeChartTab)) { + setActiveChartTab(visibleTabs[0].key); + } + }, [visibleTabs, activeChartTab, setActiveChartTab]); + + const handleUserTabsChange = useCallback((checkedValues) => { + if (checkedValues.length === 0) return; + setUserTabs(checkedValues); + localStorage.setItem(STORAGE_KEYS.CHART_TABS_USER, checkedValues.join(',')); + }, []); + + const handleResetUserTabs = useCallback(() => { + setUserTabs(null); + localStorage.removeItem(STORAGE_KEYS.CHART_TABS_USER); + }, []); + + // 用户偏好设置的可选项:仅受权限限制,不受管理员全局设置限制。 + // 优先级:user preference > admin global > show all —— 用户应能覆盖管理员默认, + // 否则当 global 为 __none__ 时 Popover 将无任何可选项,形成死锁。 + const availableTabs = useMemo(() => { + return ALL_CHART_TABS.filter((tab) => { + if (tab.adminOnly && !isAdminUser) return false; + return true; + }); + }, [isAdminUser]); + + const checkedUserTabs = userTabs || visibleTabs.map((tab) => tab.key); + return ( {t('模型数据分析')} + +
{t('图表显示设置')}
+ + {availableTabs.map((tab) => ( + + {t(tab.label)} + + ))} + + {userTabs && ( + + )} + + } + trigger='click' + position='bottomLeft' + > +