From 1c3cd7984ccebf01ab0c8aa9b5818c4155fc05f8 Mon Sep 17 00:00:00 2001 From: "Apple\\Apple" Date: Sat, 7 Jun 2025 00:53:29 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20refactor:=20Refactor=20dashboard?= =?UTF-8?q?=20statistics=20cards=20and=20charts=20layout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Consolidate 8 individual stat cards into 4 grouped cards: * Account Data (Current Balance, Historical Consumption) * Usage Statistics (Request Count, Statistics Count) * Resource Consumption (Statistics Quota, Statistics Tokens) * Performance Metrics (Average RPM, Average TPM) - Add gradient header backgrounds with white text for card titles: * Blue gradient for Account Data * Green gradient for Usage Statistics * Yellow gradient for Resource Consumption * Pink gradient for Performance Metrics - Implement mini trend charts using real API data: * Replace mock data with actual time-series data from API * Hide x and y axes to show pure trend lines * Display trends only for metrics with available historical data * Remove trend charts for Current Balance, Historical Consumption, and Request Count - Merge model analysis charts into single card: * Combine "Model Consumption Distribution" and "Model Call Count Ratio" * Use responsive grid layout (vertical on mobile, horizontal on desktop) * Update card title to "Model Data Analysis" - Optimize chart configurations: * Hide axes, legends, and tooltips for mini trend charts * Maintain color consistency between metrics and trend lines * Improve performance by processing all trend data in single API call --- web/src/components/table/ChannelsTable.js | 3 +- web/src/components/table/LogsTable.js | 3 +- web/src/components/table/MjLogsTable.js | 3 +- web/src/components/table/ModelPricing.js | 6 +- web/src/components/table/RedemptionsTable.js | 3 +- web/src/components/table/TaskLogsTable.js | 3 +- web/src/components/table/TokensTable.js | 98 +---- web/src/components/table/UsersTable.js | 3 +- web/src/i18n/locales/en.json | 7 +- web/src/pages/Detail/index.js | 364 ++++++++++++++----- 10 files changed, 297 insertions(+), 196 deletions(-) diff --git a/web/src/components/table/ChannelsTable.js b/web/src/components/table/ChannelsTable.js index c32bcff3..e1c89acf 100644 --- a/web/src/components/table/ChannelsTable.js +++ b/web/src/components/table/ChannelsTable.js @@ -1638,7 +1638,8 @@ const ChannelsTable = () => { { } - shadows='hover' + shadows='always' + bordered={false} >
{ } - shadows='hover' + shadows='always' + bordered={false} >
{ let price = parseFloat(text) * groupRatio[selectedGroup]; content = (
- ${t('模型价格')}:${price.toFixed(3)} + {t('模型价格')}:${price.toFixed(3)}
); } @@ -451,7 +451,7 @@ const ModelPricing = () => { // 搜索和操作区组件 const SearchAndActions = useMemo(() => ( - +
{ // 表格组件 const ModelTable = useMemo(() => ( - +
{
{ } - shadows='hover' + shadows='always' + bordered={false} >
{timestamp2string(timestamp)}; @@ -50,8 +43,6 @@ function renderTimestamp(timestamp) { const TokensTable = () => { const { t } = useTranslation(); - const navigate = useNavigate(); - const [userState, userDispatch] = useContext(UserContext); const renderStatus = (status, model_limits_enabled = false) => { switch (status) { @@ -431,26 +422,9 @@ const TokensTable = () => { window.open(url, '_blank'); }; - // 获取用户数据 - const getUserData = async () => { - try { - const res = await API.get(`/api/user/self`); - const { success, message, data } = res.data; - if (success) { - userDispatch({ type: 'login', payload: data }); - } else { - showError(message); - } - } catch (error) { - console.error('获取用户数据失败:', error); - showError(t('获取用户数据失败')); - } - }; + useEffect(() => { - // 获取用户数据以确保显示正确的余额和使用量 - getUserData(); - loadTokens(0) .then() .catch((reason) => { @@ -574,71 +548,6 @@ const TokensTable = () => { const renderHeader = () => (
-
- navigate('/console/topup')} - > -
- - - -
-
{t('当前余额')}
-
{renderQuota(userState?.user?.quota)}
-
-
-
- - -
- - - -
-
{t('累计消费')}
-
{renderQuota(userState?.user?.used_quota)}
-
-
-
- - -
- - - -
-
{t('请求次数')}
-
{userState?.user?.request_count || 0}
-
-
-
-
- - -
{
{ // 添加一个新的状态来存储模型-颜色映射 const [modelColors, setModelColors] = useState({}); + // 添加趋势数据状态 + const [trendData, setTrendData] = useState({ + balance: [], + usedQuota: [], + requestCount: [], + times: [], + consumeQuota: [], + tokens: [], + rpm: [], + tpm: [] + }); + + // 迷你趋势图配置 + const getTrendSpec = (data, color) => ({ + type: 'line', + data: [{ id: 'trend', values: data.map((val, idx) => ({ x: idx, y: val })) }], + xField: 'x', + yField: 'y', + height: 40, + width: 100, + axes: [ + { + orient: 'bottom', + visible: false + }, + { + orient: 'left', + visible: false + } + ], + padding: 0, + autoFit: false, + legends: { visible: false }, + tooltip: { visible: false }, + crosshair: { visible: false }, + line: { + style: { + stroke: color, + lineWidth: 2 + } + }, + point: { + visible: false + }, + background: { + fill: 'transparent' + } + }); + // 显示搜索Modal const showSearchModal = () => { setSearchModalVisible(true); @@ -282,12 +331,75 @@ const Detail = (props) => { let uniqueModels = new Set(); let totalTokens = 0; - // 收集所有唯一的模型名称 + // 趋势数据处理 + let timePoints = []; + let timeQuotaMap = new Map(); + let timeTokensMap = new Map(); + let timeCountMap = new Map(); + + // 收集所有唯一的模型名称和时间点 data.forEach((item) => { uniqueModels.add(item.model_name); totalTokens += item.token_used; totalQuota += item.quota; totalTimes += item.count; + + // 记录时间点 + const timeKey = timestamp2string1(item.created_at, dataExportDefaultTime); + if (!timePoints.includes(timeKey)) { + timePoints.push(timeKey); + } + + // 按时间点累加数据 + if (!timeQuotaMap.has(timeKey)) { + timeQuotaMap.set(timeKey, 0); + timeTokensMap.set(timeKey, 0); + timeCountMap.set(timeKey, 0); + } + timeQuotaMap.set(timeKey, timeQuotaMap.get(timeKey) + item.quota); + timeTokensMap.set(timeKey, timeTokensMap.get(timeKey) + item.token_used); + timeCountMap.set(timeKey, timeCountMap.get(timeKey) + item.count); + }); + + // 确保时间点有序 + timePoints.sort(); + + // 生成趋势数据 + const quotaTrend = timePoints.map(time => timeQuotaMap.get(time) || 0); + const tokensTrend = timePoints.map(time => timeTokensMap.get(time) || 0); + const countTrend = timePoints.map(time => timeCountMap.get(time) || 0); + + // 计算RPM和TPM趋势 + const rpmTrend = []; + const tpmTrend = []; + + if (timePoints.length >= 2) { + const interval = dataExportDefaultTime === 'hour' + ? 60 // 分钟/小时 + : dataExportDefaultTime === 'day' + ? 1440 // 分钟/天 + : 10080; // 分钟/周 + + for (let i = 0; i < timePoints.length; i++) { + rpmTrend.push(timeCountMap.get(timePoints[i]) / interval); + tpmTrend.push(timeTokensMap.get(timePoints[i]) / interval); + } + } + + // 更新趋势数据状态 + setTrendData({ + // 账户数据不在API返回中,保持空数组 + balance: [], + usedQuota: [], + // 使用统计 + requestCount: [], // 没有总请求次数趋势数据 + times: countTrend, + // 资源消耗 + consumeQuota: quotaTrend, + tokens: tokensTrend, + // 性能指标 + rpm: rpmTrend, + tpm: tpmTrend }); // 处理颜色映射 @@ -336,10 +448,10 @@ const Detail = (props) => { })); // 生成时间点序列 - let timePoints = Array.from( + let chartTimePoints = Array.from( new Set([...aggregatedData.values()].map((d) => d.time)), ); - if (timePoints.length < 7) { + if (chartTimePoints.length < 7) { const lastTime = Math.max(...data.map((item) => item.created_at)); const interval = dataExportDefaultTime === 'hour' @@ -348,13 +460,13 @@ const Detail = (props) => { ? 86400 : 604800; - timePoints = Array.from({ length: 7 }, (_, i) => + chartTimePoints = Array.from({ length: 7 }, (_, i) => timestamp2string1(lastTime - (6 - i) * interval, dataExportDefaultTime), ); } // 生成柱状图数据 - timePoints.forEach((time) => { + chartTimePoints.forEach((time) => { // 为每个时间点收集所有模型的数据 let timeData = Array.from(uniqueModels).map((model) => { const key = `${time}-${model}`; @@ -441,71 +553,103 @@ const Detail = (props) => { }, []); // 数据卡片信息 - const statsData = [ + const groupedStatsData = [ { - title: t('当前余额'), - value: renderQuota(userState?.user?.quota), - icon: , + title: t('账户数据'), color: 'bg-blue-50', - avatarColor: 'blue', - onClick: () => navigate('/console/topup'), + items: [ + { + title: t('当前余额'), + value: renderQuota(userState?.user?.quota), + icon: , + avatarColor: 'blue', + onClick: () => navigate('/console/topup'), + trendData: [], // 当前余额没有趋势数据 + trendColor: '#3b82f6' + }, + { + title: t('历史消耗'), + value: renderQuota(userState?.user?.used_quota), + icon: , + avatarColor: 'purple', + trendData: [], // 历史消耗没有趋势数据 + trendColor: '#8b5cf6' + } + ] }, { - title: t('历史消耗'), - value: renderQuota(userState?.user?.used_quota), - icon: , - color: 'bg-purple-50', - avatarColor: 'purple', - }, - { - title: t('请求次数'), - value: userState.user?.request_count, - icon: , + title: t('使用统计'), color: 'bg-green-50', - avatarColor: 'green', + items: [ + { + title: t('请求次数'), + value: userState.user?.request_count, + icon: , + avatarColor: 'green', + trendData: [], // 请求次数没有趋势数据 + trendColor: '#10b981' + }, + { + title: t('统计次数'), + value: times, + icon: , + avatarColor: 'cyan', + trendData: trendData.times, + trendColor: '#06b6d4' + } + ] }, { - title: t('统计额度'), - value: renderQuota(consumeQuota), - icon: , + title: t('资源消耗'), color: 'bg-yellow-50', - avatarColor: 'yellow', + items: [ + { + title: t('统计额度'), + value: renderQuota(consumeQuota), + icon: , + avatarColor: 'yellow', + trendData: trendData.consumeQuota, + trendColor: '#f59e0b' + }, + { + title: t('统计Tokens'), + value: isNaN(consumeTokens) ? 0 : consumeTokens, + icon: , + avatarColor: 'pink', + trendData: trendData.tokens, + trendColor: '#ec4899' + } + ] }, { - title: t('统计Tokens'), - value: isNaN(consumeTokens) ? 0 : consumeTokens, - icon: , - color: 'bg-pink-50', - avatarColor: 'pink', - }, - { - title: t('统计次数'), - value: times, - icon: , - color: 'bg-teal-50', - avatarColor: 'cyan', - }, - { - title: t('平均RPM'), - value: ( - times / - ((Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000) - ).toFixed(3), - icon: , + title: t('性能指标'), color: 'bg-indigo-50', - avatarColor: 'indigo', - }, - { - title: t('平均TPM'), - value: (() => { - const tpm = consumeTokens / - ((Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000); - return isNaN(tpm) ? '0' : tpm.toFixed(3); - })(), - icon: , - color: 'bg-orange-50', - avatarColor: 'orange', - }, + items: [ + { + title: t('平均RPM'), + value: ( + times / + ((Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000) + ).toFixed(3), + icon: , + avatarColor: 'indigo', + trendData: trendData.rpm, + trendColor: '#6366f1' + }, + { + title: t('平均TPM'), + value: (() => { + const tpm = consumeTokens / + ((Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000); + return isNaN(tpm) ? '0' : tpm.toFixed(3); + })(), + icon: , + avatarColor: 'orange', + trendData: trendData.tpm, + trendColor: '#f97316' + } + ] + } ]; // 获取问候语 @@ -612,48 +756,84 @@ const Detail = (props) => {
- {statsData.map((stat, idx) => ( + {groupedStatsData.map((group, idx) => ( {group.title}
} + headerStyle={{ + background: idx === 0 + ? 'linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%)' + : idx === 1 + ? 'linear-gradient(135deg, #10b981 0%, #34d399 100%)' + : idx === 2 + ? 'linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%)' + : 'linear-gradient(135deg, #ec4899 0%, #f472b6 100%)', + borderTopLeftRadius: '16px', + borderTopRightRadius: '16px', + padding: '12px 16px', + }} > -
- - {stat.icon} - -
-
{stat.title}
-
{stat.value}
-
+
+ {group.items.map((item, itemIdx) => ( +
+
+ + {item.icon} + +
+
{item.title}
+
{item.value}
+
+
+ {item.trendData && item.trendData.length > 0 && ( +
+ +
+ )} +
+ ))}
))}
-
- -
- -
-
- - -
- +
+ +
+
+ +
+
+ +