🎨 refactor: Refactor dashboard statistics cards and charts layout
- 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
This commit is contained in:
@@ -1638,7 +1638,8 @@ const ChannelsTable = () => {
|
|||||||
<Card
|
<Card
|
||||||
className="!rounded-2xl overflow-hidden"
|
className="!rounded-2xl overflow-hidden"
|
||||||
title={renderHeader()}
|
title={renderHeader()}
|
||||||
shadows='hover'
|
shadows='always'
|
||||||
|
bordered={false}
|
||||||
>
|
>
|
||||||
<Table
|
<Table
|
||||||
columns={getVisibleColumns()}
|
columns={getVisibleColumns()}
|
||||||
|
|||||||
@@ -1259,7 +1259,8 @@ const LogsTable = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
shadows='hover'
|
shadows='always'
|
||||||
|
bordered={false}
|
||||||
>
|
>
|
||||||
<Table
|
<Table
|
||||||
columns={getVisibleColumns()}
|
columns={getVisibleColumns()}
|
||||||
|
|||||||
@@ -864,7 +864,8 @@ const LogsTable = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
shadows='hover'
|
shadows='always'
|
||||||
|
bordered={false}
|
||||||
>
|
>
|
||||||
<Table
|
<Table
|
||||||
columns={getVisibleColumns()}
|
columns={getVisibleColumns()}
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ const ModelPricing = () => {
|
|||||||
let price = parseFloat(text) * groupRatio[selectedGroup];
|
let price = parseFloat(text) * groupRatio[selectedGroup];
|
||||||
content = (
|
content = (
|
||||||
<div className="text-gray-700">
|
<div className="text-gray-700">
|
||||||
${t('模型价格')}:${price.toFixed(3)}
|
{t('模型价格')}:${price.toFixed(3)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -451,7 +451,7 @@ const ModelPricing = () => {
|
|||||||
|
|
||||||
// 搜索和操作区组件
|
// 搜索和操作区组件
|
||||||
const SearchAndActions = useMemo(() => (
|
const SearchAndActions = useMemo(() => (
|
||||||
<Card className="!rounded-xl mb-6" shadows='hover'>
|
<Card className="!rounded-xl mb-6" bordered={false}>
|
||||||
<div className="flex flex-wrap items-center gap-4">
|
<div className="flex flex-wrap items-center gap-4">
|
||||||
<div className="flex-1 min-w-[200px]">
|
<div className="flex-1 min-w-[200px]">
|
||||||
<Input
|
<Input
|
||||||
@@ -482,7 +482,7 @@ const ModelPricing = () => {
|
|||||||
|
|
||||||
// 表格组件
|
// 表格组件
|
||||||
const ModelTable = useMemo(() => (
|
const ModelTable = useMemo(() => (
|
||||||
<Card className="!rounded-xl overflow-hidden" shadows='hover'>
|
<Card className="!rounded-xl overflow-hidden" bordered={false}>
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={filteredModels}
|
dataSource={filteredModels}
|
||||||
|
|||||||
@@ -501,7 +501,8 @@ const RedemptionsTable = () => {
|
|||||||
<Card
|
<Card
|
||||||
className="!rounded-2xl overflow-hidden"
|
className="!rounded-2xl overflow-hidden"
|
||||||
title={renderHeader()}
|
title={renderHeader()}
|
||||||
shadows='hover'
|
shadows='always'
|
||||||
|
bordered={false}
|
||||||
>
|
>
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -702,7 +702,8 @@ const LogsTable = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
shadows='hover'
|
shadows='always'
|
||||||
|
bordered={false}
|
||||||
>
|
>
|
||||||
<Table
|
<Table
|
||||||
columns={getVisibleColumns()}
|
columns={getVisibleColumns()}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React, { useEffect, useState, useContext } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import {
|
import {
|
||||||
API,
|
API,
|
||||||
copy,
|
copy,
|
||||||
@@ -21,8 +20,6 @@ import {
|
|||||||
Table,
|
Table,
|
||||||
Tag,
|
Tag,
|
||||||
Input,
|
Input,
|
||||||
Divider,
|
|
||||||
Avatar,
|
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -36,13 +33,9 @@ import {
|
|||||||
IconStop,
|
IconStop,
|
||||||
IconPlay,
|
IconPlay,
|
||||||
IconMore,
|
IconMore,
|
||||||
IconMoneyExchangeStroked,
|
|
||||||
IconHistogram,
|
|
||||||
IconRotate,
|
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
import EditToken from '../../pages/Token/EditToken';
|
import EditToken from '../../pages/Token/EditToken';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { UserContext } from '../../context/User';
|
|
||||||
|
|
||||||
function renderTimestamp(timestamp) {
|
function renderTimestamp(timestamp) {
|
||||||
return <>{timestamp2string(timestamp)}</>;
|
return <>{timestamp2string(timestamp)}</>;
|
||||||
@@ -50,8 +43,6 @@ function renderTimestamp(timestamp) {
|
|||||||
|
|
||||||
const TokensTable = () => {
|
const TokensTable = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
|
||||||
const [userState, userDispatch] = useContext(UserContext);
|
|
||||||
|
|
||||||
const renderStatus = (status, model_limits_enabled = false) => {
|
const renderStatus = (status, model_limits_enabled = false) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -431,26 +422,9 @@ const TokensTable = () => {
|
|||||||
window.open(url, '_blank');
|
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(() => {
|
useEffect(() => {
|
||||||
// 获取用户数据以确保显示正确的余额和使用量
|
|
||||||
getUserData();
|
|
||||||
|
|
||||||
loadTokens(0)
|
loadTokens(0)
|
||||||
.then()
|
.then()
|
||||||
.catch((reason) => {
|
.catch((reason) => {
|
||||||
@@ -574,71 +548,6 @@ const TokensTable = () => {
|
|||||||
|
|
||||||
const renderHeader = () => (
|
const renderHeader = () => (
|
||||||
<div className="flex flex-col w-full">
|
<div className="flex flex-col w-full">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
||||||
<Card
|
|
||||||
shadows='hover'
|
|
||||||
className="bg-blue-50 border-0 !rounded-2xl w-full"
|
|
||||||
headerLine={false}
|
|
||||||
onClick={() => navigate('/console/topup')}
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Avatar
|
|
||||||
className="mr-3"
|
|
||||||
size="medium"
|
|
||||||
color="blue"
|
|
||||||
>
|
|
||||||
<IconMoneyExchangeStroked size="large" />
|
|
||||||
</Avatar>
|
|
||||||
<div>
|
|
||||||
<div className="text-sm text-gray-500">{t('当前余额')}</div>
|
|
||||||
<div className="text-xl font-semibold">{renderQuota(userState?.user?.quota)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card
|
|
||||||
shadows='hover'
|
|
||||||
className="bg-purple-50 border-0 !rounded-2xl w-full"
|
|
||||||
headerLine={false}
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Avatar
|
|
||||||
className="mr-3"
|
|
||||||
size="medium"
|
|
||||||
color="purple"
|
|
||||||
>
|
|
||||||
<IconHistogram size="large" />
|
|
||||||
</Avatar>
|
|
||||||
<div>
|
|
||||||
<div className="text-sm text-gray-500">{t('累计消费')}</div>
|
|
||||||
<div className="text-xl font-semibold">{renderQuota(userState?.user?.used_quota)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card
|
|
||||||
shadows='hover'
|
|
||||||
className="bg-green-50 border-0 !rounded-2xl w-full"
|
|
||||||
headerLine={false}
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Avatar
|
|
||||||
className="mr-3"
|
|
||||||
size="medium"
|
|
||||||
color="green"
|
|
||||||
>
|
|
||||||
<IconRotate size="large" />
|
|
||||||
</Avatar>
|
|
||||||
<div>
|
|
||||||
<div className="text-sm text-gray-500">{t('请求次数')}</div>
|
|
||||||
<div className="text-xl font-semibold">{userState?.user?.request_count || 0}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Divider margin="12px" />
|
|
||||||
|
|
||||||
<div className="flex flex-col md:flex-row justify-between items-center gap-4 w-full">
|
<div className="flex flex-col md:flex-row justify-between items-center gap-4 w-full">
|
||||||
<div className="flex gap-2 w-full md:w-auto order-2 md:order-1">
|
<div className="flex gap-2 w-full md:w-auto order-2 md:order-1">
|
||||||
<Button
|
<Button
|
||||||
@@ -723,7 +632,8 @@ const TokensTable = () => {
|
|||||||
<Card
|
<Card
|
||||||
className="!rounded-2xl overflow-hidden"
|
className="!rounded-2xl overflow-hidden"
|
||||||
title={renderHeader()}
|
title={renderHeader()}
|
||||||
shadows='hover'
|
shadows='always'
|
||||||
|
bordered={false}
|
||||||
>
|
>
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -551,7 +551,8 @@ const UsersTable = () => {
|
|||||||
<Card
|
<Card
|
||||||
className="!rounded-2xl overflow-hidden"
|
className="!rounded-2xl overflow-hidden"
|
||||||
title={renderHeader()}
|
title={renderHeader()}
|
||||||
shadows='hover'
|
shadows='always'
|
||||||
|
bordered={false}
|
||||||
>
|
>
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -1551,5 +1551,10 @@
|
|||||||
"提供基础功能演示,方便用户了解系统特性。": "Provide basic feature demonstrations to help users understand the system features.",
|
"提供基础功能演示,方便用户了解系统特性。": "Provide basic feature demonstrations to help users understand the system features.",
|
||||||
"适用于为多个用户提供服务的场景": "Suitable for scenarios where multiple users are provided.",
|
"适用于为多个用户提供服务的场景": "Suitable for scenarios where multiple users are provided.",
|
||||||
"适用于个人使用的场景,不需要设置模型价格": "Suitable for personal use, no need to set model price.",
|
"适用于个人使用的场景,不需要设置模型价格": "Suitable for personal use, no need to set model price.",
|
||||||
"适用于展示系统功能的场景,提供基础功能演示": "Suitable for scenarios where the system functions are displayed, providing basic feature demonstrations."
|
"适用于展示系统功能的场景,提供基础功能演示": "Suitable for scenarios where the system functions are displayed, providing basic feature demonstrations.",
|
||||||
|
"账户数据": "Account Data",
|
||||||
|
"使用统计": "Usage Statistics",
|
||||||
|
"资源消耗": "Resource Consumption",
|
||||||
|
"性能指标": "Performance Indicators",
|
||||||
|
"模型数据分析": "Model Data Analysis"
|
||||||
}
|
}
|
||||||
@@ -208,6 +208,55 @@ const Detail = (props) => {
|
|||||||
// 添加一个新的状态来存储模型-颜色映射
|
// 添加一个新的状态来存储模型-颜色映射
|
||||||
const [modelColors, setModelColors] = useState({});
|
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
|
// 显示搜索Modal
|
||||||
const showSearchModal = () => {
|
const showSearchModal = () => {
|
||||||
setSearchModalVisible(true);
|
setSearchModalVisible(true);
|
||||||
@@ -282,12 +331,75 @@ const Detail = (props) => {
|
|||||||
let uniqueModels = new Set();
|
let uniqueModels = new Set();
|
||||||
let totalTokens = 0;
|
let totalTokens = 0;
|
||||||
|
|
||||||
// 收集所有唯一的模型名称
|
// 趋势数据处理
|
||||||
|
let timePoints = [];
|
||||||
|
let timeQuotaMap = new Map();
|
||||||
|
let timeTokensMap = new Map();
|
||||||
|
let timeCountMap = new Map();
|
||||||
|
|
||||||
|
// 收集所有唯一的模型名称和时间点
|
||||||
data.forEach((item) => {
|
data.forEach((item) => {
|
||||||
uniqueModels.add(item.model_name);
|
uniqueModels.add(item.model_name);
|
||||||
totalTokens += item.token_used;
|
totalTokens += item.token_used;
|
||||||
totalQuota += item.quota;
|
totalQuota += item.quota;
|
||||||
totalTimes += item.count;
|
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)),
|
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 lastTime = Math.max(...data.map((item) => item.created_at));
|
||||||
const interval =
|
const interval =
|
||||||
dataExportDefaultTime === 'hour'
|
dataExportDefaultTime === 'hour'
|
||||||
@@ -348,13 +460,13 @@ const Detail = (props) => {
|
|||||||
? 86400
|
? 86400
|
||||||
: 604800;
|
: 604800;
|
||||||
|
|
||||||
timePoints = Array.from({ length: 7 }, (_, i) =>
|
chartTimePoints = Array.from({ length: 7 }, (_, i) =>
|
||||||
timestamp2string1(lastTime - (6 - i) * interval, dataExportDefaultTime),
|
timestamp2string1(lastTime - (6 - i) * interval, dataExportDefaultTime),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成柱状图数据
|
// 生成柱状图数据
|
||||||
timePoints.forEach((time) => {
|
chartTimePoints.forEach((time) => {
|
||||||
// 为每个时间点收集所有模型的数据
|
// 为每个时间点收集所有模型的数据
|
||||||
let timeData = Array.from(uniqueModels).map((model) => {
|
let timeData = Array.from(uniqueModels).map((model) => {
|
||||||
const key = `${time}-${model}`;
|
const key = `${time}-${model}`;
|
||||||
@@ -441,71 +553,103 @@ const Detail = (props) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 数据卡片信息
|
// 数据卡片信息
|
||||||
const statsData = [
|
const groupedStatsData = [
|
||||||
{
|
{
|
||||||
title: t('当前余额'),
|
title: t('账户数据'),
|
||||||
value: renderQuota(userState?.user?.quota),
|
|
||||||
icon: <IconMoneyExchangeStroked size="large" />,
|
|
||||||
color: 'bg-blue-50',
|
color: 'bg-blue-50',
|
||||||
avatarColor: 'blue',
|
items: [
|
||||||
onClick: () => navigate('/console/topup'),
|
{
|
||||||
|
title: t('当前余额'),
|
||||||
|
value: renderQuota(userState?.user?.quota),
|
||||||
|
icon: <IconMoneyExchangeStroked size="large" />,
|
||||||
|
avatarColor: 'blue',
|
||||||
|
onClick: () => navigate('/console/topup'),
|
||||||
|
trendData: [], // 当前余额没有趋势数据
|
||||||
|
trendColor: '#3b82f6'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('历史消耗'),
|
||||||
|
value: renderQuota(userState?.user?.used_quota),
|
||||||
|
icon: <IconHistogram size="large" />,
|
||||||
|
avatarColor: 'purple',
|
||||||
|
trendData: [], // 历史消耗没有趋势数据
|
||||||
|
trendColor: '#8b5cf6'
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('历史消耗'),
|
title: t('使用统计'),
|
||||||
value: renderQuota(userState?.user?.used_quota),
|
|
||||||
icon: <IconHistogram size="large" />,
|
|
||||||
color: 'bg-purple-50',
|
|
||||||
avatarColor: 'purple',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('请求次数'),
|
|
||||||
value: userState.user?.request_count,
|
|
||||||
icon: <IconRotate size="large" />,
|
|
||||||
color: 'bg-green-50',
|
color: 'bg-green-50',
|
||||||
avatarColor: 'green',
|
items: [
|
||||||
|
{
|
||||||
|
title: t('请求次数'),
|
||||||
|
value: userState.user?.request_count,
|
||||||
|
icon: <IconRotate size="large" />,
|
||||||
|
avatarColor: 'green',
|
||||||
|
trendData: [], // 请求次数没有趋势数据
|
||||||
|
trendColor: '#10b981'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('统计次数'),
|
||||||
|
value: times,
|
||||||
|
icon: <IconPulse size="large" />,
|
||||||
|
avatarColor: 'cyan',
|
||||||
|
trendData: trendData.times,
|
||||||
|
trendColor: '#06b6d4'
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('统计额度'),
|
title: t('资源消耗'),
|
||||||
value: renderQuota(consumeQuota),
|
|
||||||
icon: <IconCoinMoneyStroked size="large" />,
|
|
||||||
color: 'bg-yellow-50',
|
color: 'bg-yellow-50',
|
||||||
avatarColor: 'yellow',
|
items: [
|
||||||
|
{
|
||||||
|
title: t('统计额度'),
|
||||||
|
value: renderQuota(consumeQuota),
|
||||||
|
icon: <IconCoinMoneyStroked size="large" />,
|
||||||
|
avatarColor: 'yellow',
|
||||||
|
trendData: trendData.consumeQuota,
|
||||||
|
trendColor: '#f59e0b'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('统计Tokens'),
|
||||||
|
value: isNaN(consumeTokens) ? 0 : consumeTokens,
|
||||||
|
icon: <IconTextStroked size="large" />,
|
||||||
|
avatarColor: 'pink',
|
||||||
|
trendData: trendData.tokens,
|
||||||
|
trendColor: '#ec4899'
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('统计Tokens'),
|
title: t('性能指标'),
|
||||||
value: isNaN(consumeTokens) ? 0 : consumeTokens,
|
|
||||||
icon: <IconTextStroked size="large" />,
|
|
||||||
color: 'bg-pink-50',
|
|
||||||
avatarColor: 'pink',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('统计次数'),
|
|
||||||
value: times,
|
|
||||||
icon: <IconPulse size="large" />,
|
|
||||||
color: 'bg-teal-50',
|
|
||||||
avatarColor: 'cyan',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('平均RPM'),
|
|
||||||
value: (
|
|
||||||
times /
|
|
||||||
((Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000)
|
|
||||||
).toFixed(3),
|
|
||||||
icon: <IconStopwatchStroked size="large" />,
|
|
||||||
color: 'bg-indigo-50',
|
color: 'bg-indigo-50',
|
||||||
avatarColor: 'indigo',
|
items: [
|
||||||
},
|
{
|
||||||
{
|
title: t('平均RPM'),
|
||||||
title: t('平均TPM'),
|
value: (
|
||||||
value: (() => {
|
times /
|
||||||
const tpm = consumeTokens /
|
((Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000)
|
||||||
((Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000);
|
).toFixed(3),
|
||||||
return isNaN(tpm) ? '0' : tpm.toFixed(3);
|
icon: <IconStopwatchStroked size="large" />,
|
||||||
})(),
|
avatarColor: 'indigo',
|
||||||
icon: <IconTypograph size="large" />,
|
trendData: trendData.rpm,
|
||||||
color: 'bg-orange-50',
|
trendColor: '#6366f1'
|
||||||
avatarColor: 'orange',
|
},
|
||||||
},
|
{
|
||||||
|
title: t('平均TPM'),
|
||||||
|
value: (() => {
|
||||||
|
const tpm = consumeTokens /
|
||||||
|
((Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000);
|
||||||
|
return isNaN(tpm) ? '0' : tpm.toFixed(3);
|
||||||
|
})(),
|
||||||
|
icon: <IconTypograph size="large" />,
|
||||||
|
avatarColor: 'orange',
|
||||||
|
trendData: trendData.tpm,
|
||||||
|
trendColor: '#f97316'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
// 获取问候语
|
// 获取问候语
|
||||||
@@ -612,48 +756,84 @@ const Detail = (props) => {
|
|||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
{statsData.map((stat, idx) => (
|
{groupedStatsData.map((group, idx) => (
|
||||||
<Card
|
<Card
|
||||||
key={idx}
|
key={idx}
|
||||||
shadows='hover'
|
shadows='always'
|
||||||
className={`${stat.color} border-0 !rounded-2xl w-full`}
|
bordered={false}
|
||||||
headerLine={false}
|
className={`${group.color} border-0 !rounded-2xl w-full`}
|
||||||
onClick={stat.onClick}
|
headerLine={true}
|
||||||
|
header={<div style={{ color: 'white', fontWeight: 'bold', fontSize: '16px' }}>{group.title}</div>}
|
||||||
|
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',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center">
|
<div className="space-y-4">
|
||||||
<Avatar
|
{group.items.map((item, itemIdx) => (
|
||||||
className="mr-3"
|
<div
|
||||||
size="medium"
|
key={itemIdx}
|
||||||
color={stat.avatarColor}
|
className="flex items-center justify-between cursor-pointer"
|
||||||
>
|
onClick={item.onClick}
|
||||||
{stat.icon}
|
>
|
||||||
</Avatar>
|
<div className="flex items-center">
|
||||||
<div>
|
<Avatar
|
||||||
<div className="text-sm text-gray-500">{stat.title}</div>
|
className="mr-3"
|
||||||
<div className="text-xl font-semibold">{stat.value}</div>
|
size="small"
|
||||||
</div>
|
color={item.avatarColor}
|
||||||
|
>
|
||||||
|
{item.icon}
|
||||||
|
</Avatar>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-gray-500">{item.title}</div>
|
||||||
|
<div className="text-lg font-semibold">{item.value}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{item.trendData && item.trendData.length > 0 && (
|
||||||
|
<div className="w-24 h-10">
|
||||||
|
<VChart
|
||||||
|
spec={getTrendSpec(item.trendData, item.trendColor)}
|
||||||
|
option={{ mode: 'desktop-browser' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
<div className="grid grid-cols-1 lg:grid-cols-1 gap-6 mb-6">
|
||||||
<Card shadows='hover' className="shadow-sm !rounded-2xl" headerLine={true} title={t('模型消耗分布')}>
|
<Card
|
||||||
<div style={{ height: 400 }}>
|
shadows='always'
|
||||||
<VChart
|
bordered={false}
|
||||||
spec={spec_line}
|
className="shadow-sm !rounded-2xl"
|
||||||
option={{ mode: 'desktop-browser' }}
|
headerLine={true}
|
||||||
/>
|
title={t('模型数据分析')}
|
||||||
</div>
|
>
|
||||||
</Card>
|
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
||||||
|
<div style={{ height: 400 }}>
|
||||||
<Card shadows='hover' className="shadow-sm !rounded-2xl" headerLine={true} title={t('模型调用次数占比')}>
|
<VChart
|
||||||
<div style={{ height: 400 }}>
|
spec={spec_line}
|
||||||
<VChart
|
option={{ mode: 'desktop-browser' }}
|
||||||
spec={spec_pie}
|
/>
|
||||||
option={{ mode: 'desktop-browser' }}
|
</div>
|
||||||
/>
|
<div style={{ height: 400 }}>
|
||||||
|
<VChart
|
||||||
|
spec={spec_pie}
|
||||||
|
option={{ mode: 'desktop-browser' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user