From 9e17b07b849e32807014cb4d78ff9659f16f4f3b Mon Sep 17 00:00:00 2001 From: taoliang1 Date: Wed, 15 Apr 2026 15:53:48 +0800 Subject: [PATCH 1/5] feat(dashboard): add token usage view with model filter - Add model_name filter parameter to all /api/data/ endpoints (backend) - Add model name input field to dashboard SearchModal (frontend) - Add new "Token Usage Distribution" stacked bar chart tab - Extend data aggregation to include token_used field - Add i18n translations for 7 locales --- controller/usedata.go | 9 +- model/usedata.go | 42 +++++--- web/src/components/dashboard/ChartsPanel.jsx | 5 + web/src/components/dashboard/index.jsx | 1 + .../dashboard/modals/SearchModal.jsx | 11 +- web/src/helpers/dashboard.jsx | 2 + .../hooks/dashboard/useDashboardCharts.jsx | 100 ++++++++++++++++++ web/src/hooks/dashboard/useDashboardData.js | 8 +- web/src/i18n/locales/en.json | 1 + web/src/i18n/locales/fr.json | 1 + web/src/i18n/locales/ja.json | 1 + web/src/i18n/locales/ru.json | 1 + web/src/i18n/locales/vi.json | 1 + web/src/i18n/locales/zh-CN.json | 1 + web/src/i18n/locales/zh-TW.json | 1 + 15 files changed, 164 insertions(+), 21 deletions(-) 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/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/src/components/dashboard/ChartsPanel.jsx b/web/src/components/dashboard/ChartsPanel.jsx index 0034ffd90068..db2a4e2884ab 100644 --- a/web/src/components/dashboard/ChartsPanel.jsx +++ b/web/src/components/dashboard/ChartsPanel.jsx @@ -29,6 +29,7 @@ const ChartsPanel = ({ spec_model_line, spec_pie, spec_rank_bar, + spec_token_bar, spec_user_rank, spec_user_trend, isAdminUser, @@ -57,6 +58,7 @@ const ChartsPanel = ({ {t('调用趋势')}} itemKey='2' /> {t('调用次数分布')}} itemKey='3' /> {t('调用次数排行')}} itemKey='4' /> + {t('Token消耗分布')}} itemKey='7' /> {isAdminUser && ( {t('用户消耗排行')}} itemKey='5' /> )} @@ -81,6 +83,9 @@ const ChartsPanel = ({ {activeChartTab === '4' && ( )} + {activeChartTab === '7' && ( + + )} {activeChartTab === '5' && isAdminUser && ( )} diff --git a/web/src/components/dashboard/index.jsx b/web/src/components/dashboard/index.jsx index 811e23ca760f..6d44f9f784d0 100644 --- a/web/src/components/dashboard/index.jsx +++ b/web/src/components/dashboard/index.jsx @@ -194,6 +194,7 @@ const Dashboard = () => { spec_model_line={dashboardCharts.spec_model_line} spec_pie={dashboardCharts.spec_pie} spec_rank_bar={dashboardCharts.spec_rank_bar} + spec_token_bar={dashboardCharts.spec_token_bar} spec_user_rank={dashboardCharts.spec_user_rank} spec_user_trend={dashboardCharts.spec_user_trend} isAdminUser={dashboardData.isAdminUser} diff --git a/web/src/components/dashboard/modals/SearchModal.jsx b/web/src/components/dashboard/modals/SearchModal.jsx index f619831dccbb..1632b0665ff1 100644 --- a/web/src/components/dashboard/modals/SearchModal.jsx +++ b/web/src/components/dashboard/modals/SearchModal.jsx @@ -42,7 +42,7 @@ const SearchModal = ({ ); - const { start_timestamp, end_timestamp, username } = inputs; + const { start_timestamp, end_timestamp, username, model_name } = inputs; return ( handleInputChange(value, 'model_name'), + })} + {isAdminUser && createFormField(Form.Input, { field: 'username', diff --git a/web/src/helpers/dashboard.jsx b/web/src/helpers/dashboard.jsx index a7a30bf6719f..a52eaf58f839 100644 --- a/web/src/helpers/dashboard.jsx +++ b/web/src/helpers/dashboard.jsx @@ -349,12 +349,14 @@ export const aggregateDataByTimeAndModel = (data, dataExportDefaultTime) => { model: modelKey, quota: 0, count: 0, + tokenUsed: 0, }); } const existing = aggregatedData.get(key); existing.quota += item.quota; existing.count += item.count; + existing.tokenUsed += item.token_used || 0; }); return aggregatedData; diff --git a/web/src/hooks/dashboard/useDashboardCharts.jsx b/web/src/hooks/dashboard/useDashboardCharts.jsx index ef0d47b0cd25..c5773b64dccd 100644 --- a/web/src/hooks/dashboard/useDashboardCharts.jsx +++ b/web/src/hooks/dashboard/useDashboardCharts.jsx @@ -286,6 +286,76 @@ export const useDashboardCharts = ( }, }); + // ========== Token 消耗分布 ========== + const [spec_token_bar, setSpecTokenBar] = useState({ + type: 'bar', + data: [ + { + id: 'tokenBarData', + values: [], + }, + ], + xField: 'Time', + yField: 'Tokens', + seriesField: 'Model', + stack: true, + legends: { + visible: true, + selectMode: 'single', + }, + title: { + visible: true, + text: t('Token消耗分布'), + subtext: `${t('总计')}:${renderNumber(0)}`, + }, + bar: { + state: { + hover: { + stroke: '#000', + lineWidth: 1, + }, + }, + }, + tooltip: { + mark: { + content: [ + { + key: (datum) => datum['Model'], + value: (datum) => renderNumber(datum['rawTokens'] || 0), + }, + ], + }, + dimension: { + content: [ + { + key: (datum) => datum['Model'], + value: (datum) => datum['rawTokens'] || 0, + }, + ], + updateContent: (array) => { + array.sort((a, b) => b.value - a.value); + let sum = 0; + for (let i = 0; i < array.length; i++) { + let value = parseFloat(array[i].value); + if (isNaN(value)) value = 0; + if (array[i].datum && array[i].datum.TimeSum) { + sum = array[i].datum.TimeSum; + } + array[i].value = renderNumber(value); + } + array.unshift({ + key: t('总计'), + value: renderNumber(sum), + }); + return array; + }, + }, + }, + color: { + specified: modelColorMap, + }, + }); + // ========== Admin: 用户消耗排行 ========== const [spec_user_rank, setSpecUserRank] = useState({ type: 'bar', @@ -490,6 +560,35 @@ export const useDashboardCharts = ( 'barData', ); + // ===== Token 消耗分布堆叠柱状图 ===== + let tokenBarData = []; + chartTimePoints.forEach((time) => { + let timeData = Array.from(uniqueModels).map((model) => { + const key = `${time}-${model}`; + const aggregated = aggregatedData.get(key); + return { + Time: time, + Model: model, + rawTokens: aggregated?.tokenUsed || 0, + Tokens: aggregated?.tokenUsed || 0, + }; + }); + + const timeSum = timeData.reduce((sum, item) => sum + item.rawTokens, 0); + timeData.sort((a, b) => b.rawTokens - a.rawTokens); + timeData = timeData.map((item) => ({ ...item, TimeSum: timeSum })); + tokenBarData.push(...timeData); + }); + tokenBarData.sort((a, b) => a.Time.localeCompare(b.Time)); + + updateChartSpec( + setSpecTokenBar, + tokenBarData, + `${t('总计')}:${renderNumber(totalTokens)}`, + newModelColors, + 'tokenBarData', + ); + // ===== 模型调用次数折线图 ===== let modelLineData = []; chartTimePoints.forEach((time) => { @@ -619,6 +718,7 @@ export const useDashboardCharts = ( spec_line, spec_model_line, spec_rank_bar, + spec_token_bar, spec_user_rank, spec_user_trend, updateChartData, diff --git a/web/src/hooks/dashboard/useDashboardData.js b/web/src/hooks/dashboard/useDashboardData.js index e9b2cad83e72..2c969698e8f5 100644 --- a/web/src/hooks/dashboard/useDashboardData.js +++ b/web/src/hooks/dashboard/useDashboardData.js @@ -164,10 +164,11 @@ export const useDashboardData = (userState, userDispatch, statusState) => { let localStartTimestamp = Date.parse(start_timestamp) / 1000; let localEndTimestamp = Date.parse(end_timestamp) / 1000; + const modelNameParam = inputs.model_name ? `&model_name=${encodeURIComponent(inputs.model_name)}` : ''; if (isAdminUser) { - url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`; + url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}${modelNameParam}`; } else { - url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`; + url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}${modelNameParam}`; } const res = await API.get(url); @@ -219,7 +220,8 @@ export const useDashboardData = (userState, userDispatch, statusState) => { const { start_timestamp, end_timestamp } = inputs; const localStartTimestamp = Date.parse(start_timestamp) / 1000; const localEndTimestamp = Date.parse(end_timestamp) / 1000; - const url = `/api/data/users?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`; + const modelNameParam = inputs.model_name ? `&model_name=${encodeURIComponent(inputs.model_name)}` : ''; + const url = `/api/data/users?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}${modelNameParam}`; const res = await API.get(url); const { success, message, data } = res.data; if (success) { diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index eade595e55d4..1218aade2c2e 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -3086,6 +3086,7 @@ "调用次数": "Call Count", "调用次数分布": "Models call distribution", "调用次数排行": "Models call ranking", + "Token消耗分布": "Token Usage Distribution", "调用趋势": "Call trend", "调试信息": "Debug information", "谨慎": "Cautious", diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index fed6b1913105..71da9d274baa 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -3059,6 +3059,7 @@ "调用次数": "Nombre d'appels", "调用次数分布": "Distribution des appels de modèles", "调用次数排行": "Classement des appels de modèles", + "Token消耗分布": "Distribution de la consommation de tokens", "调用趋势": "Tendance des appels", "调试信息": "Informations de débogage", "谨慎": "Prudent", diff --git a/web/src/i18n/locales/ja.json b/web/src/i18n/locales/ja.json index 61641dfbfcf2..cf021595f88d 100644 --- a/web/src/i18n/locales/ja.json +++ b/web/src/i18n/locales/ja.json @@ -3040,6 +3040,7 @@ "调用次数": "呼び出し回数", "调用次数分布": "呼び出し回数分布", "调用次数排行": "呼び出し回数ランキング", + "Token消耗分布": "トークン消費分布", "调用趋势": "呼び出し推移", "调试信息": "デバッグ情報", "谨慎": "注意", diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index 0986d3360dfc..e8e3f6a5eb7e 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -3073,6 +3073,7 @@ "调用次数": "Количество вызовов", "调用次数分布": "Распределение количества вызовов", "调用次数排行": "Рейтинг количества вызовов", + "Token消耗分布": "Распределение потребления токенов", "调用趋势": "Тенденция вызовов", "调试信息": "Отладочная информация", "谨慎": "Осторожно", diff --git a/web/src/i18n/locales/vi.json b/web/src/i18n/locales/vi.json index 266fab5fb8b6..092d13016411 100644 --- a/web/src/i18n/locales/vi.json +++ b/web/src/i18n/locales/vi.json @@ -3492,6 +3492,7 @@ "调用次数": "Số lần gọi", "调用次数分布": "Phân phối số lần gọi", "调用次数排行": "Xếp hạng số lần gọi", + "Token消耗分布": "Phân bổ tiêu thụ Token", "调用趋势": "Xu hướng cuộc gọi", "调试信息": "Thông tin gỡ lỗi", "谨慎": "Thận trọng", diff --git a/web/src/i18n/locales/zh-CN.json b/web/src/i18n/locales/zh-CN.json index 80f1a962ab5c..950b442056ff 100644 --- a/web/src/i18n/locales/zh-CN.json +++ b/web/src/i18n/locales/zh-CN.json @@ -2324,6 +2324,7 @@ "调用次数": "调用次数", "调用次数分布": "调用次数分布", "调用次数排行": "调用次数排行", + "Token消耗分布": "Token消耗分布", "调用趋势": "调用趋势", "模型排行": "模型排行", "用户消耗排行": "用户消耗排行", diff --git a/web/src/i18n/locales/zh-TW.json b/web/src/i18n/locales/zh-TW.json index aaf6d33c2e57..ad19f094ebc9 100644 --- a/web/src/i18n/locales/zh-TW.json +++ b/web/src/i18n/locales/zh-TW.json @@ -2739,6 +2739,7 @@ "调用次数": "調用次數", "调用次数分布": "調用次數分佈", "调用次数排行": "調用次數排行", + "Token消耗分布": "Token消耗分佈", "调用趋势": "調用趨勢", "调试信息": "除錯訊息", "谨慎": "謹慎", From a8c2ed3a21e2847eed03a6d48fd8549a45691673 Mon Sep 17 00:00:00 2001 From: taoliang1 Date: Wed, 15 Apr 2026 16:57:40 +0800 Subject: [PATCH 2/5] feat(dashboard): add user-dimension token usage charts - Add "User Token Ranking" horizontal bar chart (admin) - Add "User Token Trend" area chart (admin) - Extend processUserData to aggregate token_used per user - Add i18n translations for 7 locales --- web/src/components/dashboard/ChartsPanel.jsx | 14 ++ web/src/components/dashboard/index.jsx | 2 + web/src/helpers/dashboard.jsx | 54 ++++++- .../hooks/dashboard/useDashboardCharts.jsx | 145 +++++++++++++++++- web/src/i18n/locales/en.json | 2 + web/src/i18n/locales/fr.json | 2 + web/src/i18n/locales/ja.json | 2 + web/src/i18n/locales/ru.json | 2 + web/src/i18n/locales/vi.json | 2 + web/src/i18n/locales/zh-CN.json | 2 + web/src/i18n/locales/zh-TW.json | 2 + 11 files changed, 216 insertions(+), 13 deletions(-) diff --git a/web/src/components/dashboard/ChartsPanel.jsx b/web/src/components/dashboard/ChartsPanel.jsx index db2a4e2884ab..4bd42628e73d 100644 --- a/web/src/components/dashboard/ChartsPanel.jsx +++ b/web/src/components/dashboard/ChartsPanel.jsx @@ -32,6 +32,8 @@ const ChartsPanel = ({ spec_token_bar, spec_user_rank, spec_user_trend, + spec_user_token_rank, + spec_user_token_trend, isAdminUser, CARD_PROPS, CHART_CONFIG, @@ -65,6 +67,12 @@ const ChartsPanel = ({ {isAdminUser && ( {t('用户消耗趋势')}} itemKey='6' /> )} + {isAdminUser && ( + {t('用户Token排行')}} itemKey='8' /> + )} + {isAdminUser && ( + {t('用户Token趋势')}} itemKey='9' /> + )} } @@ -92,6 +100,12 @@ const ChartsPanel = ({ {activeChartTab === '6' && isAdminUser && ( )} + {activeChartTab === '8' && isAdminUser && ( + + )} + {activeChartTab === '9' && isAdminUser && ( + + )} ); diff --git a/web/src/components/dashboard/index.jsx b/web/src/components/dashboard/index.jsx index 6d44f9f784d0..bf5d27869357 100644 --- a/web/src/components/dashboard/index.jsx +++ b/web/src/components/dashboard/index.jsx @@ -197,6 +197,8 @@ const Dashboard = () => { spec_token_bar={dashboardCharts.spec_token_bar} spec_user_rank={dashboardCharts.spec_user_rank} spec_user_trend={dashboardCharts.spec_user_trend} + spec_user_token_rank={dashboardCharts.spec_user_token_rank} + spec_user_token_trend={dashboardCharts.spec_user_token_trend} isAdminUser={dashboardData.isAdminUser} CARD_PROPS={CARD_PROPS} CHART_CONFIG={CHART_CONFIG} diff --git a/web/src/helpers/dashboard.jsx b/web/src/helpers/dashboard.jsx index a52eaf58f839..b67847d73a0a 100644 --- a/web/src/helpers/dashboard.jsx +++ b/web/src/helpers/dashboard.jsx @@ -393,9 +393,12 @@ export const generateChartTimePoints = ( // ========== 用户维度数据处理 ========== export const processUserData = (data, dataExportDefaultTime, limit = 10) => { const userQuotaTotal = new Map(); + const userTokenTotal = new Map(); data.forEach((item) => { - const prev = userQuotaTotal.get(item.username) || 0; - userQuotaTotal.set(item.username, prev + item.quota); + const prevQuota = userQuotaTotal.get(item.username) || 0; + userQuotaTotal.set(item.username, prevQuota + item.quota); + const prevToken = userTokenTotal.get(item.username) || 0; + userTokenTotal.set(item.username, prevToken + (item.token_used || 0)); }); const sorted = Array.from(userQuotaTotal.entries()).sort( @@ -409,9 +412,22 @@ export const processUserData = (data, dataExportDefaultTime, limit = 10) => { Quota: quota, })); + // Token 维度排行(按 token 用量排序取 top) + const sortedByToken = Array.from(userTokenTotal.entries()).sort( + (a, b) => b[1] - a[1], + ); + const topTokenUsers = sortedByToken.slice(0, limit).map(([u]) => u); + const topTokenUserSet = new Set(topTokenUsers); + + const tokenRankingData = sortedByToken.slice(0, limit).map(([username, tokens]) => ({ + User: username, + Tokens: tokens, + })); + const showYear = isDataCrossYear(data.map((item) => item.created_at)); const timeUserMap = new Map(); + const timeUserTokenMap = new Map(); const allTimePoints = new Set(); data.forEach((item) => { @@ -421,11 +437,20 @@ export const processUserData = (data, dataExportDefaultTime, limit = 10) => { showYear, ); allTimePoints.add(timeKey); - const user = topUserSet.has(item.username) ? item.username : null; - if (!user) return; - const key = `${timeKey}-${user}`; - const prev = timeUserMap.get(key) || { quota: 0 }; - timeUserMap.set(key, { quota: prev.quota + item.quota }); + + // Quota 趋势 + if (topUserSet.has(item.username)) { + const key = `${timeKey}-${item.username}`; + const prev = timeUserMap.get(key) || { quota: 0 }; + timeUserMap.set(key, { quota: prev.quota + item.quota }); + } + + // Token 趋势 + if (topTokenUserSet.has(item.username)) { + const key = `${timeKey}-${item.username}`; + const prev = timeUserTokenMap.get(key) || { tokens: 0 }; + timeUserTokenMap.set(key, { tokens: prev.tokens + (item.token_used || 0) }); + } }); const sortedTimePoints = Array.from(allTimePoints).sort(); @@ -442,5 +467,18 @@ export const processUserData = (data, dataExportDefaultTime, limit = 10) => { }); }); - return { rankingData, trendData, topUsers }; + const tokenTrendData = []; + sortedTimePoints.forEach((time) => { + topTokenUsers.forEach((user) => { + const key = `${time}-${user}`; + const val = timeUserTokenMap.get(key); + tokenTrendData.push({ + Time: time, + User: user, + Tokens: val?.tokens || 0, + }); + }); + }); + + return { rankingData, trendData, topUsers, tokenRankingData, tokenTrendData, topTokenUsers }; }; diff --git a/web/src/hooks/dashboard/useDashboardCharts.jsx b/web/src/hooks/dashboard/useDashboardCharts.jsx index c5773b64dccd..236c01dc9c0b 100644 --- a/web/src/hooks/dashboard/useDashboardCharts.jsx +++ b/web/src/hooks/dashboard/useDashboardCharts.jsx @@ -453,6 +453,103 @@ export const useDashboardCharts = ( color: { type: 'ordinal', range: USER_COLORS }, }); + // ========== Admin: 用户Token排行 ========== + const [spec_user_token_rank, setSpecUserTokenRank] = useState({ + type: 'bar', + data: [{ id: 'userTokenRankData', values: [] }], + xField: 'Tokens', + yField: 'User', + seriesField: 'User', + direction: 'horizontal', + legends: { visible: false }, + title: { + visible: true, + text: t('用户Token排行'), + subtext: '', + }, + bar: { + state: { hover: { stroke: '#000', lineWidth: 1 } }, + }, + label: { + visible: true, + position: 'outside', + formatMethod: (value, datum) => renderNumber(datum['Tokens'] || 0), + }, + axes: [{ + orient: 'left', + type: 'band', + label: { visible: true }, + }, { + orient: 'bottom', + type: 'linear', + visible: false, + }], + tooltip: { + mark: { + content: [{ + key: (datum) => datum['User'], + value: (datum) => renderNumber(datum['Tokens'] || 0), + }], + }, + }, + color: { type: 'ordinal', range: USER_COLORS }, + }); + + // ========== Admin: 用户Token趋势 ========== + const [spec_user_token_trend, setSpecUserTokenTrend] = useState({ + type: 'area', + data: [{ id: 'userTokenTrendData', values: [] }], + xField: 'Time', + yField: 'Tokens', + seriesField: 'User', + stack: false, + legends: { visible: true, selectMode: 'single' }, + title: { + visible: true, + text: t('用户Token趋势'), + subtext: '', + }, + axes: [{ + orient: 'left', + label: { + formatMethod: (value) => renderNumber(value), + }, + }], + area: { style: { fillOpacity: 0.15 } }, + line: { style: { lineWidth: 2 } }, + point: { visible: false }, + tooltip: { + mark: { + content: [{ + key: (datum) => datum['User'], + value: (datum) => renderNumber(datum['Tokens'] || 0), + }], + }, + dimension: { + content: [{ + key: (datum) => datum['User'], + value: (datum) => datum['Tokens'] || 0, + }], + updateContent: (array) => { + array.sort((a, b) => b.value - a.value); + let sum = 0; + for (let i = 0; i < array.length; i++) { + let value = parseFloat(array[i].value); + if (isNaN(value)) value = 0; + sum += value; + array[i].value = renderNumber(value); + } + array.unshift({ + key: t('总计'), + value: renderNumber(sum), + }); + return array; + }, + }, + }, + color: { type: 'ordinal', range: USER_COLORS }, + }); + // ========== 数据处理函数 ========== const generateModelColors = useCallback((uniqueModels, modelColors) => { const newModelColors = {}; @@ -664,12 +761,14 @@ export const useDashboardCharts = ( // ========== 用户维度图表数据处理 ========== const updateUserChartData = useCallback( (data) => { - const { rankingData, trendData: userTrend } = processUserData( - data, - dataExportDefaultTime, - 10, - ); + const { + rankingData, + trendData: userTrend, + tokenRankingData, + tokenTrendData, + } = processUserData(data, dataExportDefaultTime, 10); + // ===== 用户消耗排行(Quota)===== const userRankValues = rankingData.map((item) => ({ User: item.User, rawQuota: item.Quota, @@ -687,6 +786,7 @@ export const useDashboardCharts = ( }, })); + // ===== 用户消耗趋势(Quota)===== const userTrendValues = userTrend.map((item) => ({ Time: item.Time, User: item.User, @@ -702,6 +802,39 @@ export const useDashboardCharts = ( subtext: `${t('总计')}:${renderQuota(totalUserQuota, 2)}`, }, })); + + // ===== 用户Token排行 ===== + const userTokenRankValues = tokenRankingData.map((item) => ({ + User: item.User, + Tokens: item.Tokens, + })).sort((a, b) => b.Tokens - a.Tokens); + + const totalUserTokens = tokenRankingData.reduce((s, i) => s + i.Tokens, 0); + + setSpecUserTokenRank((prev) => ({ + ...prev, + data: [{ id: 'userTokenRankData', values: userTokenRankValues }], + title: { + ...prev.title, + subtext: `${t('总计')}:${renderNumber(totalUserTokens)}`, + }, + })); + + // ===== 用户Token趋势 ===== + const userTokenTrendValues = tokenTrendData.map((item) => ({ + Time: item.Time, + User: item.User, + Tokens: item.Tokens, + })); + + setSpecUserTokenTrend((prev) => ({ + ...prev, + data: [{ id: 'userTokenTrendData', values: userTokenTrendValues }], + title: { + ...prev.title, + subtext: `${t('总计')}:${renderNumber(totalUserTokens)}`, + }, + })); }, [dataExportDefaultTime, t], ); @@ -721,6 +854,8 @@ export const useDashboardCharts = ( spec_token_bar, spec_user_rank, spec_user_trend, + spec_user_token_rank, + spec_user_token_trend, updateChartData, updateUserChartData, generateModelColors, diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 1218aade2c2e..bf2b65c0d380 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -3087,6 +3087,8 @@ "调用次数分布": "Models call distribution", "调用次数排行": "Models call ranking", "Token消耗分布": "Token Usage Distribution", + "用户Token排行": "User Token Ranking", + "用户Token趋势": "User Token Trend", "调用趋势": "Call trend", "调试信息": "Debug information", "谨慎": "Cautious", diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index 71da9d274baa..42cbc3b9c4b3 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -3060,6 +3060,8 @@ "调用次数分布": "Distribution des appels de modèles", "调用次数排行": "Classement des appels de modèles", "Token消耗分布": "Distribution de la consommation de tokens", + "用户Token排行": "Classement des tokens par utilisateur", + "用户Token趋势": "Tendance des tokens par utilisateur", "调用趋势": "Tendance des appels", "调试信息": "Informations de débogage", "谨慎": "Prudent", diff --git a/web/src/i18n/locales/ja.json b/web/src/i18n/locales/ja.json index cf021595f88d..34e3b08962eb 100644 --- a/web/src/i18n/locales/ja.json +++ b/web/src/i18n/locales/ja.json @@ -3041,6 +3041,8 @@ "调用次数分布": "呼び出し回数分布", "调用次数排行": "呼び出し回数ランキング", "Token消耗分布": "トークン消費分布", + "用户Token排行": "ユーザートークンランキング", + "用户Token趋势": "ユーザートークン推移", "调用趋势": "呼び出し推移", "调试信息": "デバッグ情報", "谨慎": "注意", diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index e8e3f6a5eb7e..4e7e61e1c3c2 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -3074,6 +3074,8 @@ "调用次数分布": "Распределение количества вызовов", "调用次数排行": "Рейтинг количества вызовов", "Token消耗分布": "Распределение потребления токенов", + "用户Token排行": "Рейтинг токенов по пользователям", + "用户Token趋势": "Тенденция токенов по пользователям", "调用趋势": "Тенденция вызовов", "调试信息": "Отладочная информация", "谨慎": "Осторожно", diff --git a/web/src/i18n/locales/vi.json b/web/src/i18n/locales/vi.json index 092d13016411..622be0cbb313 100644 --- a/web/src/i18n/locales/vi.json +++ b/web/src/i18n/locales/vi.json @@ -3493,6 +3493,8 @@ "调用次数分布": "Phân phối số lần gọi", "调用次数排行": "Xếp hạng số lần gọi", "Token消耗分布": "Phân bổ tiêu thụ Token", + "用户Token排行": "Xếp hạng Token theo người dùng", + "用户Token趋势": "Xu hướng Token theo người dùng", "调用趋势": "Xu hướng cuộc gọi", "调试信息": "Thông tin gỡ lỗi", "谨慎": "Thận trọng", diff --git a/web/src/i18n/locales/zh-CN.json b/web/src/i18n/locales/zh-CN.json index 950b442056ff..073f4eae99dc 100644 --- a/web/src/i18n/locales/zh-CN.json +++ b/web/src/i18n/locales/zh-CN.json @@ -2325,6 +2325,8 @@ "调用次数分布": "调用次数分布", "调用次数排行": "调用次数排行", "Token消耗分布": "Token消耗分布", + "用户Token排行": "用户Token排行", + "用户Token趋势": "用户Token趋势", "调用趋势": "调用趋势", "模型排行": "模型排行", "用户消耗排行": "用户消耗排行", diff --git a/web/src/i18n/locales/zh-TW.json b/web/src/i18n/locales/zh-TW.json index ad19f094ebc9..9e7632e15c08 100644 --- a/web/src/i18n/locales/zh-TW.json +++ b/web/src/i18n/locales/zh-TW.json @@ -2740,6 +2740,8 @@ "调用次数分布": "調用次數分佈", "调用次数排行": "調用次數排行", "Token消耗分布": "Token消耗分佈", + "用户Token排行": "用戶Token排行", + "用户Token趋势": "用戶Token趨勢", "调用趋势": "調用趨勢", "调试信息": "除錯訊息", "谨慎": "謹慎", From a20c0db5ba4cb5f7dc89e71392ea66c81c3621dd Mon Sep 17 00:00:00 2001 From: taoliang1 Date: Wed, 15 Apr 2026 18:05:26 +0800 Subject: [PATCH 3/5] feat(dashboard): add chart tab visibility control - Add DataDashboardChartTabs global setting (admin, comma-separated keys) - Add CheckboxGroup in dashboard settings for admin to select visible tabs - Add per-user tab preference via Popover settings button (localStorage) - Priority: user preference > admin global > show all - Add i18n translations for 7 locales --- common/constants.go | 5 +- controller/misc.go | 1 + model/option.go | 3 + web/src/components/dashboard/ChartsPanel.jsx | 173 +++++++++++++----- .../components/settings/DashboardSetting.jsx | 1 + web/src/constants/dashboard.constants.js | 15 ++ web/src/helpers/data.js | 4 + web/src/i18n/locales/en.json | 2 + web/src/i18n/locales/fr.json | 2 + web/src/i18n/locales/ja.json | 2 + web/src/i18n/locales/ru.json | 2 + web/src/i18n/locales/vi.json | 2 + web/src/i18n/locales/zh-CN.json | 2 + web/src/i18n/locales/zh-TW.json | 2 + .../Dashboard/SettingsDataDashboard.jsx | 35 +++- 15 files changed, 201 insertions(+), 50 deletions(-) diff --git a/common/constants.go b/common/constants.go index 6caa7f5c0007..b57e022871ef 100644 --- a/common/constants.go +++ b/common/constants.go @@ -27,8 +27,9 @@ var DrawingEnabled = true var TaskEnabled = true var DataExportEnabled = true var DataExportInterval = 5 // unit: minute -var DataExportDefaultTime = "hour" // unit: minute -var DefaultCollapseSidebar = false // default value of collapse sidebar +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 519caed57b81..aafe534050f3 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/model/option.go b/model/option.go index efa8c01daa7b..9d0e96dcc153 100644 --- a/model/option.go +++ b/model/option.go @@ -147,6 +147,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) @@ -463,6 +464,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/web/src/components/dashboard/ChartsPanel.jsx b/web/src/components/dashboard/ChartsPanel.jsx index 4bd42628e73d..99eff15b88af 100644 --- a/web/src/components/dashboard/ChartsPanel.jsx +++ b/web/src/components/dashboard/ChartsPanel.jsx @@ -17,10 +17,23 @@ 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, STORAGE_KEYS } from '../../constants/dashboard.constants'; + +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, @@ -41,6 +54,67 @@ 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 = globalSetting ? globalSetting.split(',') : null; + 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); + }, []); + + // 用户偏好设置的可选项(受管理员全局设置和权限限制) + const availableTabs = useMemo(() => { + const globalSetting = localStorage.getItem(STORAGE_KEYS.CHART_TABS_GLOBAL) || ''; + const globalTabs = globalSetting ? globalSetting.split(',') : null; + + return ALL_CHART_TABS.filter((tab) => { + if (tab.adminOnly && !isAdminUser) return false; + if (globalTabs && !globalTabs.includes(tab.key)) 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' + > + +
- {t('消耗分布')}} itemKey='1' /> - {t('调用趋势')}} itemKey='2' /> - {t('调用次数分布')}} itemKey='3' /> - {t('调用次数排行')}} itemKey='4' /> - {t('Token消耗分布')}} itemKey='7' /> - {isAdminUser && ( - {t('用户消耗排行')}} itemKey='5' /> - )} - {isAdminUser && ( - {t('用户消耗趋势')}} itemKey='6' /> - )} - {isAdminUser && ( - {t('用户Token排行')}} itemKey='8' /> - )} - {isAdminUser && ( - {t('用户Token趋势')}} itemKey='9' /> - )} + {visibleTabs.map((tab) => ( + {t(tab.label)}} + itemKey={tab.key} + /> + ))} } bodyStyle={{ padding: 0 }} >
- {activeChartTab === '1' && ( - - )} - {activeChartTab === '2' && ( - - )} - {activeChartTab === '3' && ( - - )} - {activeChartTab === '4' && ( - - )} - {activeChartTab === '7' && ( - - )} - {activeChartTab === '5' && isAdminUser && ( - - )} - {activeChartTab === '6' && isAdminUser && ( - - )} - {activeChartTab === '8' && isAdminUser && ( - - )} - {activeChartTab === '9' && isAdminUser && ( - - )} + {visibleTabs.map((tab) => { + if (activeChartTab !== tab.key) return null; + const specKey = SPEC_MAP[tab.key]; + return ( + + ); + })}
); diff --git a/web/src/components/settings/DashboardSetting.jsx b/web/src/components/settings/DashboardSetting.jsx index 7bf4249437ab..829956a6b11f 100644 --- a/web/src/components/settings/DashboardSetting.jsx +++ b/web/src/components/settings/DashboardSetting.jsx @@ -48,6 +48,7 @@ const DashboardSetting = () => { DataExportEnabled: false, DataExportDefaultTime: 'hour', DataExportInterval: 5, + DataDashboardChartTabs: '', }); let [loading, setLoading] = useState(false); diff --git a/web/src/constants/dashboard.constants.js b/web/src/constants/dashboard.constants.js index 7e930b479d57..229e9952465a 100644 --- a/web/src/constants/dashboard.constants.js +++ b/web/src/constants/dashboard.constants.js @@ -138,8 +138,23 @@ export const UPTIME_STATUS_MAP = { export const STORAGE_KEYS = { DATA_EXPORT_DEFAULT_TIME: 'data_export_default_time', MJ_NOTIFY_ENABLED: 'mj_notify_enabled', + CHART_TABS_USER: 'dashboard_chart_tabs_user', + CHART_TABS_GLOBAL: 'data_dashboard_chart_tabs', }; +// ========== 图表 Tab 元信息 ========== +export const ALL_CHART_TABS = [ + { key: '1', label: '消耗分布', adminOnly: false }, + { key: '2', label: '调用趋势', adminOnly: false }, + { key: '3', label: '调用次数分布', adminOnly: false }, + { key: '4', label: '调用次数排行', adminOnly: false }, + { key: '7', label: 'Token消耗分布', adminOnly: false }, + { key: '5', label: '用户消耗排行', adminOnly: true }, + { key: '6', label: '用户消耗趋势', adminOnly: true }, + { key: '8', label: '用户Token排行', adminOnly: true }, + { key: '9', label: '用户Token趋势', adminOnly: true }, +]; + // ========== 默认值 ========== export const DEFAULTS = { PAGE_SIZE: 20, diff --git a/web/src/helpers/data.js b/web/src/helpers/data.js index e45aac3e9941..c2d91dd90d19 100644 --- a/web/src/helpers/data.js +++ b/web/src/helpers/data.js @@ -34,6 +34,10 @@ export function setStatusData(data) { 'data_export_default_time', data.data_export_default_time, ); + localStorage.setItem( + 'data_dashboard_chart_tabs', + data.data_dashboard_chart_tabs || '', + ); localStorage.setItem( 'default_collapse_sidebar', data.default_collapse_sidebar, diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index bf2b65c0d380..2f9e6afb7fc0 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1996,6 +1996,8 @@ "模型排行": "Model ranking", "模型支持的接口端点信息": "Model supported API endpoint information", "模型数据分析": "Model Data Analysis", + "图表显示设置": "Chart Display Settings", + "可见图表": "Visible Charts", "模型映射必须是合法的 JSON 格式!": "Model mapping must be in valid JSON format!", "模型更新成功!": "Model updated successfully!", "模型未加入列表,可能无法调用": "Model not in the list; requests may fail", diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index 42cbc3b9c4b3..f63e35d5f43f 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -1978,6 +1978,8 @@ "模型排行": "Classement des modèles", "模型支持的接口端点信息": "Informations sur les points de terminaison de l'API pris en charge par le modèle", "模型数据分析": "Analyse des données du modèle", + "图表显示设置": "Paramètres d'affichage des graphiques", + "可见图表": "Graphiques visibles", "模型映射必须是合法的 JSON 格式!": "Le mappage de modèles doit être au format JSON valide !", "模型更新成功!": "Modèle mis à jour avec succès !", "模型未加入列表,可能无法调用": "Le modèle n'est pas dans la liste, il peut ne pas être disponible", diff --git a/web/src/i18n/locales/ja.json b/web/src/i18n/locales/ja.json index 34e3b08962eb..6724c7683293 100644 --- a/web/src/i18n/locales/ja.json +++ b/web/src/i18n/locales/ja.json @@ -1961,6 +1961,8 @@ "模型排行": "モデルランキング", "模型支持的接口端点信息": "モデルが対応するAPIエンドポイント情報", "模型数据分析": "モデルデータ分析", + "图表显示设置": "チャート表示設定", + "可见图表": "表示するチャート", "模型映射必须是合法的 JSON 格式!": "モデルマッピングは、有効なJSON形式である必要があります", "模型更新成功!": "モデルの更新に成功しました!", "模型未加入列表,可能无法调用": "Model not in the list; requests may fail", diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index 4e7e61e1c3c2..c1f24738ec69 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -1990,6 +1990,8 @@ "模型排行": "Рейтинг моделей", "模型支持的接口端点信息": "Информация о конечных точках интерфейса, поддерживаемых моделью", "模型数据分析": "Анализ данных моделей", + "图表显示设置": "Настройки отображения графиков", + "可见图表": "Видимые графики", "模型映射必须是合法的 JSON 格式!": "Сопоставление моделей должно быть в допустимом формате JSON!", "模型更新成功!": "Модель успешно обновлена!", "模型未加入列表,可能无法调用": "Модель не добавлена в список, вызовы могут не работать", diff --git a/web/src/i18n/locales/vi.json b/web/src/i18n/locales/vi.json index 622be0cbb313..3f1e2b672650 100644 --- a/web/src/i18n/locales/vi.json +++ b/web/src/i18n/locales/vi.json @@ -1971,6 +1971,8 @@ "模型排行": "Xếp hạng mô hình", "模型支持的接口端点信息": "Thông tin điểm cuối API được mô hình hỗ trợ", "模型数据分析": "Phân tích dữ liệu mô hình", + "图表显示设置": "Cài đặt hiển thị biểu đồ", + "可见图表": "Biểu đồ hiển thị", "模型映射": "Ánh xạ mô hình", "模型映射关系": "Quan hệ ánh xạ mô hình", "模型映射必须是合法的 JSON 格式!": "Ánh xạ mô hình phải ở định dạng JSON hợp lệ!", diff --git a/web/src/i18n/locales/zh-CN.json b/web/src/i18n/locales/zh-CN.json index 073f4eae99dc..df3c859a9191 100644 --- a/web/src/i18n/locales/zh-CN.json +++ b/web/src/i18n/locales/zh-CN.json @@ -1470,6 +1470,8 @@ "模型拉取失败: {{error}}": "模型拉取失败: {{error}}", "模型支持的接口端点信息": "模型支持的接口端点信息", "模型数据分析": "模型数据分析", + "图表显示设置": "图表显示设置", + "可见图表": "可见图表", "模型映射必须是合法的 JSON 格式!": "模型映射必须是合法的 JSON 格式!", "模型更新成功!": "模型更新成功!", "模型未加入列表,可能无法调用": "模型未加入列表,可能无法调用", diff --git a/web/src/i18n/locales/zh-TW.json b/web/src/i18n/locales/zh-TW.json index 9e7632e15c08..4d7208e5141c 100644 --- a/web/src/i18n/locales/zh-TW.json +++ b/web/src/i18n/locales/zh-TW.json @@ -1752,6 +1752,8 @@ "模型排行": "模型排行", "模型支持的接口端点信息": "模型支援的接口端點資訊", "模型数据分析": "模型數據分析", + "图表显示设置": "圖表顯示設定", + "可见图表": "可見圖表", "模型映射必须是合法的 JSON 格式!": "模型映射必須是合法的 JSON 格式!", "模型更新成功!": "模型更新成功!", "模型未加入列表,可能无法调用": "模型未加入列表,可能無法調用", diff --git a/web/src/pages/Setting/Dashboard/SettingsDataDashboard.jsx b/web/src/pages/Setting/Dashboard/SettingsDataDashboard.jsx index c33ba77acde4..f79814323c8e 100644 --- a/web/src/pages/Setting/Dashboard/SettingsDataDashboard.jsx +++ b/web/src/pages/Setting/Dashboard/SettingsDataDashboard.jsx @@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com */ import React, { useEffect, useState, useRef } from 'react'; -import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui'; +import { Button, Col, Form, Row, Spin, CheckboxGroup, Checkbox } from '@douyinfe/semi-ui'; import { compareObjects, API, @@ -27,6 +27,7 @@ import { showWarning, } from '../../../helpers'; import { useTranslation } from 'react-i18next'; +import { ALL_CHART_TABS } from '../../../constants/dashboard.constants'; export default function DataDashboard(props) { const { t } = useTranslation(); @@ -41,6 +42,7 @@ export default function DataDashboard(props) { DataExportEnabled: false, DataExportInterval: '', DataExportDefaultTime: '', + DataDashboardChartTabs: '', }); const refForm = useRef(); const [inputsRow, setInputsRow] = useState(inputs); @@ -157,6 +159,37 @@ export default function DataDashboard(props) { /> + + + + tab.key) + } + onChange={(checkedValues) => { + const allKeys = ALL_CHART_TABS.map((tab) => tab.key); + const isAll = checkedValues.length === allKeys.length; + setInputs({ + ...inputs, + DataDashboardChartTabs: isAll + ? '' + : checkedValues.join(','), + }); + }} + > + {ALL_CHART_TABS.map((tab) => ( + + {t(tab.label)} + {tab.adminOnly ? ` (${t('管理员')})` : ''} + + ))} + + + +