♻️Refactor: Detail Page
This commit is contained in:
@@ -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"
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user