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