♻️Refactor: Detail Page

This commit is contained in:
Apple\Apple
2025-05-20 18:01:38 +08:00
parent c6d7cc7c25
commit 64973e6cff
2 changed files with 226 additions and 165 deletions

View File

@@ -1406,5 +1406,9 @@
"New API项目仓库地址": "New API project repository address: ", "New API项目仓库地址": "New API project repository address: ",
"NewAPI © {{currentYear}} QuantumNous | 基于 One API v0.5.4 © 2023 JustSong。": "NewAPI © {{currentYear}} QuantumNous | Based on One API v0.5.4 © 2023 JustSong.", "NewAPI © {{currentYear}} QuantumNous | 基于 One API v0.5.4 © 2023 JustSong。": "NewAPI © {{currentYear}} QuantumNous | Based on One API v0.5.4 © 2023 JustSong.",
"本项目根据MIT许可证授权需在遵守Apache-2.0协议的前提下使用。": "This project is licensed under the MIT License and must be used in compliance with the Apache-2.0 License.", "本项目根据MIT许可证授权需在遵守Apache-2.0协议的前提下使用。": "This project is licensed under the MIT License and must be used in compliance with the Apache-2.0 License.",
"管理员暂时未设置任何关于内容": "The administrator has not set any custom About content yet" "管理员暂时未设置任何关于内容": "The administrator has not set any custom About content yet",
"早上好": "Good morning",
"中午好": "Good afternoon",
"下午好": "Good afternoon",
"晚上好": "Good evening"
} }

View File

@@ -2,16 +2,14 @@ import React, { useContext, useEffect, useRef, useState } from 'react';
import { initVChartSemiTheme } from '@visactor/vchart-semi-theme'; import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
import { import {
Button,
Card, Card,
Col,
Descriptions,
Form, Form,
Layout,
Row,
Spin, Spin,
Tabs, Typography,
IconButton,
Modal,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { IconRefresh, IconSearch } from '@douyinfe/semi-icons';
import { VChart } from '@visactor/react-vchart'; import { VChart } from '@visactor/react-vchart';
import { import {
API, API,
@@ -25,8 +23,6 @@ import {
modelColorMap, modelColorMap,
renderNumber, renderNumber,
renderQuota, renderQuota,
renderQuotaNumberWithDigit,
stringToColor,
modelToColor, modelToColor,
} from '../../helpers/render'; } from '../../helpers/render';
import { UserContext } from '../../context/User/index.js'; import { UserContext } from '../../context/User/index.js';
@@ -35,6 +31,7 @@ import { useTranslation } from 'react-i18next';
const Detail = (props) => { const Detail = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { Text } = Typography;
const formRef = useRef(); const formRef = useRef();
let now = new Date(); let now = new Date();
const [userState, userDispatch] = useContext(UserContext); const [userState, userDispatch] = useContext(UserContext);
@@ -67,6 +64,8 @@ const Detail = (props) => {
); );
const [pieData, setPieData] = useState([{ type: 'null', value: '0' }]); const [pieData, setPieData] = useState([{ type: 'null', value: '0' }]);
const [lineData, setLineData] = useState([]); const [lineData, setLineData] = useState([]);
const [searchModalVisible, setSearchModalVisible] = useState(false);
const [spec_pie, setSpecPie] = useState({ const [spec_pie, setSpecPie] = useState({
type: 'pie', type: 'pie',
data: [ data: [
@@ -200,6 +199,22 @@ const Detail = (props) => {
// 添加一个新的状态来存储模型-颜色映射 // 添加一个新的状态来存储模型-颜色映射
const [modelColors, setModelColors] = useState({}); const [modelColors, setModelColors] = useState({});
// 显示搜索Modal
const showSearchModal = () => {
setSearchModalVisible(true);
};
// 关闭搜索Modal
const handleCloseModal = () => {
setSearchModalVisible(false);
};
// 搜索Modal确认按钮
const handleSearchConfirm = () => {
refresh();
setSearchModalVisible(false);
};
const handleInputChange = (value, name) => { const handleInputChange = (value, name) => {
if (name === 'data_export_default_time') { if (name === 'data_export_default_time') {
setDataExportDefaultTime(value); setDataExportDefaultTime(value);
@@ -416,165 +431,207 @@ const Detail = (props) => {
} }
}, []); }, []);
// 数据卡片信息
const statsData = [
{
title: t('当前余额'),
value: renderQuota(userState?.user?.quota),
icon: '💰',
color: 'bg-blue-50',
},
{
title: t('历史消耗'),
value: renderQuota(userState?.user?.used_quota),
icon: '📊',
color: 'bg-purple-50',
},
{
title: t('请求次数'),
value: userState.user?.request_count,
icon: '🔄',
color: 'bg-green-50',
},
{
title: t('统计额度'),
value: renderQuota(consumeQuota),
icon: '💲',
color: 'bg-yellow-50',
},
{
title: t('统计Tokens'),
value: isNaN(consumeTokens) ? 0 : consumeTokens,
icon: '🔤',
color: 'bg-pink-50',
},
{
title: t('统计次数'),
value: times,
icon: '📈',
color: 'bg-teal-50',
},
{
title: t('平均RPM'),
value: (
times /
((Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000)
).toFixed(3),
icon: '⏱️',
color: 'bg-indigo-50',
},
{
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',
},
];
// 获取问候语
const getGreeting = () => {
const hours = new Date().getHours();
let greeting = '';
if (hours >= 5 && hours < 12) {
greeting = t('早上好');
} else if (hours >= 12 && hours < 14) {
greeting = t('中午好');
} else if (hours >= 14 && hours < 18) {
greeting = t('下午好');
} else {
greeting = t('晚上好');
}
const username = userState?.user?.username || '';
return `👋${greeting}${username}`;
};
return ( return (
<> <div className="bg-gray-50 min-h-screen">
<Layout> <div className="flex items-center justify-between mb-6">
<Layout.Header> <h2 className="text-2xl font-semibold text-gray-800">{getGreeting()}</h2>
<h3>{t('数据看板')}</h3> <div className="flex gap-3">
</Layout.Header> <IconButton
<Layout.Content> icon={<IconSearch />}
<Form ref={formRef} layout='horizontal' style={{ marginTop: 10 }}> onClick={showSearchModal}
<> className="bg-green-500 text-white hover:bg-green-600 !rounded-full"
<Form.DatePicker size="large"
field='start_timestamp' />
label={t('起始时间')} <IconButton
style={{ width: 272 }} icon={<IconRefresh />}
initValue={start_timestamp} onClick={refresh}
value={start_timestamp} loading={loading}
type='dateTime' className="bg-blue-500 text-white hover:bg-blue-600 !rounded-full"
name='start_timestamp' size="large"
onChange={(value) => />
handleInputChange(value, 'start_timestamp') </div>
} </div>
/>
<Form.DatePicker {/* 搜索条件Modal */}
field='end_timestamp' <Modal
fluid title={t('搜索条件')}
label={t('结束时间')} visible={searchModalVisible}
style={{ width: 272 }} onOk={handleSearchConfirm}
initValue={end_timestamp} onCancel={handleCloseModal}
value={end_timestamp} closeOnEsc={true}
type='dateTime' width={700}
name='end_timestamp' centered
onChange={(value) => handleInputChange(value, 'end_timestamp')} >
/> <Form ref={formRef} layout='vertical' className="w-full">
<Form.Select <Form.DatePicker
field='data_export_default_time' field='start_timestamp'
label={t('时间粒度')} label={t('起始时间')}
style={{ width: 176 }} className="w-full mb-4"
initValue={dataExportDefaultTime} initValue={start_timestamp}
placeholder={t('时间粒度')} value={start_timestamp}
name='data_export_default_time' type='dateTime'
optionList={[ name='start_timestamp'
{ label: t('小时'), value: 'hour' }, onChange={(value) => handleInputChange(value, 'start_timestamp')}
{ label: t('天'), value: 'day' }, />
{ label: t('周'), value: 'week' }, <Form.DatePicker
]} field='end_timestamp'
onChange={(value) => label={t('结束时间')}
handleInputChange(value, 'data_export_default_time') className="w-full mb-4"
} initValue={end_timestamp}
></Form.Select> value={end_timestamp}
{isAdminUser && ( type='dateTime'
<> name='end_timestamp'
<Form.Input onChange={(value) => handleInputChange(value, 'end_timestamp')}
field='username' />
label={t('用户名称')} <Form.Select
style={{ width: 176 }} field='data_export_default_time'
value={username} label={t('时间粒度')}
placeholder={t('可选值')} className="w-full mb-4"
name='username' initValue={dataExportDefaultTime}
onChange={(value) => handleInputChange(value, 'username')} placeholder={t('时间粒度')}
/> name='data_export_default_time'
</> optionList={[
)} { label: t('小时'), value: 'hour' },
<Button { label: t('天'), value: 'day' },
label={t('查询')} { label: t(''), value: 'week' },
type='primary' ]}
htmlType='submit' onChange={(value) => handleInputChange(value, 'data_export_default_time')}
className='btn-margin-right' />
onClick={refresh} {isAdminUser && (
loading={loading} <Form.Input
style={{ marginTop: 24 }} field='username'
label={t('用户名称')}
className="w-full mb-4"
value={username}
placeholder={t('可选值')}
name='username'
onChange={(value) => handleInputChange(value, 'username')}
/>
)}
</Form>
</Modal>
<Spin spinning={loading}>
<div className="mb-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{statsData.map((stat, idx) => (
<Card
key={idx}
shadows='hover'
className={`${stat.color} border-0 !rounded-2xl w-full`}
headerLine={false}
> >
{t('查询')} <div className="flex items-center">
</Button> <div className="text-2xl mr-3">{stat.icon}</div>
<Form.Section></Form.Section> <div>
</> <div className="text-sm text-gray-500">{stat.title}</div>
</Form> <div className="text-xl font-semibold">{stat.value}</div>
<Spin spinning={loading}>
<Row
gutter={{ xs: 16, sm: 16, md: 16, lg: 24, xl: 24, xxl: 24 }}
style={{ marginTop: 20 }}
type='flex'
justify='space-between'
>
<Col span={styleState.isMobile ? 24 : 8}>
<Card className='panel-desc-card'>
<Descriptions row size='small'>
<Descriptions.Item itemKey={t('当前余额')}>
{renderQuota(userState?.user?.quota)}
</Descriptions.Item>
<Descriptions.Item itemKey={t('历史消耗')}>
{renderQuota(userState?.user?.used_quota)}
</Descriptions.Item>
<Descriptions.Item itemKey={t('请求次数')}>
{userState.user?.request_count}
</Descriptions.Item>
</Descriptions>
</Card>
</Col>
<Col span={styleState.isMobile ? 24 : 8}>
<Card>
<Descriptions row size='small'>
<Descriptions.Item itemKey={t('统计额度')}>
{renderQuota(consumeQuota)}
</Descriptions.Item>
<Descriptions.Item itemKey={t('统计Tokens')}>
{consumeTokens}
</Descriptions.Item>
<Descriptions.Item itemKey={t('统计次数')}>
{times}
</Descriptions.Item>
</Descriptions>
</Card>
</Col>
<Col span={styleState.isMobile ? 24 : 8}>
<Card>
<Descriptions row size='small'>
<Descriptions.Item itemKey={t('平均RPM')}>
{(
times /
((Date.parse(end_timestamp) -
Date.parse(start_timestamp)) /
60000)
).toFixed(3)}
</Descriptions.Item>
<Descriptions.Item itemKey={t('平均TPM')}>
{(
consumeTokens /
((Date.parse(end_timestamp) -
Date.parse(start_timestamp)) /
60000)
).toFixed(3)}
</Descriptions.Item>
</Descriptions>
</Card>
</Col>
</Row>
<Card style={{ marginTop: 20 }}>
<Tabs type='line' defaultActiveKey='1'>
<Tabs.TabPane tab={t('消耗分布')} itemKey='1'>
<div style={{ height: 500 }}>
<VChart
spec={spec_line}
option={{ mode: 'desktop-browser' }}
/>
</div> </div>
</Tabs.TabPane> </div>
<Tabs.TabPane tab={t('调用次数分布')} itemKey='2'> </Card>
<div style={{ height: 500 }}> ))}
<VChart </div>
spec={spec_pie} </div>
option={{ mode: 'desktop-browser' }}
/> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
</div> <Card shadows='hover' className="shadow-sm !rounded-2xl" headerLine={true} title={t('模型消耗分布')}>
</Tabs.TabPane> <div style={{ height: 400 }}>
</Tabs> <VChart
</Card> spec={spec_line}
</Spin> option={{ mode: 'desktop-browser' }}
</Layout.Content> />
</Layout> </div>
</> </Card>
<Card shadows='hover' className="shadow-sm !rounded-2xl" headerLine={true} title={t('模型调用次数占比')}>
<div style={{ height: 400 }}>
<VChart
spec={spec_pie}
option={{ mode: 'desktop-browser' }}
/>
</div>
</Card>
</div>
</Spin>
</div>
); );
}; };