♻️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: ",
"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.",
"管理员暂时未设置任何关于内容": "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 {
Button,
Card,
Col,
Descriptions,
Form,
Layout,
Row,
Spin,
Tabs,
Typography,
IconButton,
Modal,
} from '@douyinfe/semi-ui';
import { IconRefresh, IconSearch } from '@douyinfe/semi-icons';
import { VChart } from '@visactor/react-vchart';
import {
API,
@@ -25,8 +23,6 @@ import {
modelColorMap,
renderNumber,
renderQuota,
renderQuotaNumberWithDigit,
stringToColor,
modelToColor,
} from '../../helpers/render';
import { UserContext } from '../../context/User/index.js';
@@ -35,6 +31,7 @@ import { useTranslation } from 'react-i18next';
const Detail = (props) => {
const { t } = useTranslation();
const { Text } = Typography;
const formRef = useRef();
let now = new Date();
const [userState, userDispatch] = useContext(UserContext);
@@ -67,6 +64,8 @@ const Detail = (props) => {
);
const [pieData, setPieData] = useState([{ type: 'null', value: '0' }]);
const [lineData, setLineData] = useState([]);
const [searchModalVisible, setSearchModalVisible] = useState(false);
const [spec_pie, setSpecPie] = useState({
type: 'pie',
data: [
@@ -200,6 +199,22 @@ const Detail = (props) => {
// 添加一个新的状态来存储模型-颜色映射
const [modelColors, setModelColors] = useState({});
// 显示搜索Modal
const showSearchModal = () => {
setSearchModalVisible(true);
};
// 关闭搜索Modal
const handleCloseModal = () => {
setSearchModalVisible(false);
};
// 搜索Modal确认按钮
const handleSearchConfirm = () => {
refresh();
setSearchModalVisible(false);
};
const handleInputChange = (value, name) => {
if (name === 'data_export_default_time') {
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 (
<>
<Layout>
<Layout.Header>
<h3>{t('数据看板')}</h3>
</Layout.Header>
<Layout.Content>
<Form ref={formRef} layout='horizontal' style={{ marginTop: 10 }}>
<>
<Form.DatePicker
field='start_timestamp'
label={t('起始时间')}
style={{ width: 272 }}
initValue={start_timestamp}
value={start_timestamp}
type='dateTime'
name='start_timestamp'
onChange={(value) =>
handleInputChange(value, 'start_timestamp')
}
/>
<Form.DatePicker
field='end_timestamp'
fluid
label={t('结束时间')}
style={{ width: 272 }}
initValue={end_timestamp}
value={end_timestamp}
type='dateTime'
name='end_timestamp'
onChange={(value) => handleInputChange(value, 'end_timestamp')}
/>
<Form.Select
field='data_export_default_time'
label={t('时间粒度')}
style={{ width: 176 }}
initValue={dataExportDefaultTime}
placeholder={t('时间粒度')}
name='data_export_default_time'
optionList={[
{ label: t('小时'), value: 'hour' },
{ label: t('天'), value: 'day' },
{ label: t('周'), value: 'week' },
]}
onChange={(value) =>
handleInputChange(value, 'data_export_default_time')
}
></Form.Select>
{isAdminUser && (
<>
<Form.Input
field='username'
label={t('用户名称')}
style={{ width: 176 }}
value={username}
placeholder={t('可选值')}
name='username'
onChange={(value) => handleInputChange(value, 'username')}
/>
</>
)}
<Button
label={t('查询')}
type='primary'
htmlType='submit'
className='btn-margin-right'
onClick={refresh}
loading={loading}
style={{ marginTop: 24 }}
<div className="bg-gray-50 min-h-screen">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-semibold text-gray-800">{getGreeting()}</h2>
<div className="flex gap-3">
<IconButton
icon={<IconSearch />}
onClick={showSearchModal}
className="bg-green-500 text-white hover:bg-green-600 !rounded-full"
size="large"
/>
<IconButton
icon={<IconRefresh />}
onClick={refresh}
loading={loading}
className="bg-blue-500 text-white hover:bg-blue-600 !rounded-full"
size="large"
/>
</div>
</div>
{/* 搜索条件Modal */}
<Modal
title={t('搜索条件')}
visible={searchModalVisible}
onOk={handleSearchConfirm}
onCancel={handleCloseModal}
closeOnEsc={true}
width={700}
centered
>
<Form ref={formRef} layout='vertical' className="w-full">
<Form.DatePicker
field='start_timestamp'
label={t('起始时间')}
className="w-full mb-4"
initValue={start_timestamp}
value={start_timestamp}
type='dateTime'
name='start_timestamp'
onChange={(value) => handleInputChange(value, 'start_timestamp')}
/>
<Form.DatePicker
field='end_timestamp'
label={t('结束时间')}
className="w-full mb-4"
initValue={end_timestamp}
value={end_timestamp}
type='dateTime'
name='end_timestamp'
onChange={(value) => handleInputChange(value, 'end_timestamp')}
/>
<Form.Select
field='data_export_default_time'
label={t('时间粒度')}
className="w-full mb-4"
initValue={dataExportDefaultTime}
placeholder={t('时间粒度')}
name='data_export_default_time'
optionList={[
{ label: t('小时'), value: 'hour' },
{ label: t('天'), value: 'day' },
{ label: t(''), value: 'week' },
]}
onChange={(value) => handleInputChange(value, 'data_export_default_time')}
/>
{isAdminUser && (
<Form.Input
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('查询')}
</Button>
<Form.Section></Form.Section>
</>
</Form>
<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 className="flex items-center">
<div className="text-2xl mr-3">{stat.icon}</div>
<div>
<div className="text-sm text-gray-500">{stat.title}</div>
<div className="text-xl font-semibold">{stat.value}</div>
</div>
</Tabs.TabPane>
<Tabs.TabPane tab={t('调用次数分布')} itemKey='2'>
<div style={{ height: 500 }}>
<VChart
spec={spec_pie}
option={{ mode: 'desktop-browser' }}
/>
</div>
</Tabs.TabPane>
</Tabs>
</Card>
</Spin>
</Layout.Content>
</Layout>
</>
</div>
</Card>
))}
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<Card shadows='hover' className="shadow-sm !rounded-2xl" headerLine={true} title={t('模型消耗分布')}>
<div style={{ height: 400 }}>
<VChart
spec={spec_line}
option={{ mode: 'desktop-browser' }}
/>
</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>
);
};