mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-11-16 21:23:44 +08:00
feat: add new theme berry (#860)
* feat: add theme berry * docs: add development notes * fix: fix blank page * chore: update implementation * fix: fix package.json * chore: update ui copy --------- Co-authored-by: JustSong <songquanpeng@foxmail.com>
This commit is contained in:
169
web/berry/src/views/Dashboard/component/StatisticalBarChart.js
Normal file
169
web/berry/src/views/Dashboard/component/StatisticalBarChart.js
Normal file
@@ -0,0 +1,169 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// material-ui
|
||||
import { Grid, Typography } from '@mui/material';
|
||||
|
||||
// third-party
|
||||
import Chart from 'react-apexcharts';
|
||||
|
||||
// project imports
|
||||
import SkeletonTotalGrowthBarChart from 'ui-component/cards/Skeleton/TotalGrowthBarChart';
|
||||
import MainCard from 'ui-component/cards/MainCard';
|
||||
import { gridSpacing } from 'store/constant';
|
||||
import { Box } from '@mui/material';
|
||||
|
||||
// ==============================|| DASHBOARD DEFAULT - TOTAL GROWTH BAR CHART ||============================== //
|
||||
|
||||
const StatisticalBarChart = ({ isLoading, chartDatas }) => {
|
||||
chartData.options.xaxis.categories = chartDatas.xaxis;
|
||||
chartData.series = chartDatas.data;
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<SkeletonTotalGrowthBarChart />
|
||||
) : (
|
||||
<MainCard>
|
||||
<Grid container spacing={gridSpacing}>
|
||||
<Grid item xs={12}>
|
||||
<Grid container alignItems="center" justifyContent="space-between">
|
||||
<Grid item>
|
||||
<Typography variant="h3">统计</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
{chartData.series ? (
|
||||
<Chart {...chartData} />
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '490px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<Typography variant="h3" color={'#697586'}>
|
||||
暂无数据
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</MainCard>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
StatisticalBarChart.propTypes = {
|
||||
isLoading: PropTypes.bool
|
||||
};
|
||||
|
||||
export default StatisticalBarChart;
|
||||
|
||||
const chartData = {
|
||||
height: 480,
|
||||
type: 'bar',
|
||||
options: {
|
||||
colors: [
|
||||
'#008FFB',
|
||||
'#00E396',
|
||||
'#FEB019',
|
||||
'#FF4560',
|
||||
'#775DD0',
|
||||
'#55efc4',
|
||||
'#81ecec',
|
||||
'#74b9ff',
|
||||
'#a29bfe',
|
||||
'#00b894',
|
||||
'#00cec9',
|
||||
'#0984e3',
|
||||
'#6c5ce7',
|
||||
'#ffeaa7',
|
||||
'#fab1a0',
|
||||
'#ff7675',
|
||||
'#fd79a8',
|
||||
'#fdcb6e',
|
||||
'#e17055',
|
||||
'#d63031',
|
||||
'#e84393'
|
||||
],
|
||||
chart: {
|
||||
id: 'bar-chart',
|
||||
stacked: true,
|
||||
toolbar: {
|
||||
show: true
|
||||
},
|
||||
zoom: {
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
responsive: [
|
||||
{
|
||||
breakpoint: 480,
|
||||
options: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
offsetX: -10,
|
||||
offsetY: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: false,
|
||||
columnWidth: '50%'
|
||||
}
|
||||
},
|
||||
xaxis: {
|
||||
type: 'category',
|
||||
categories: []
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
fontSize: '14px',
|
||||
fontFamily: `'Roboto', sans-serif`,
|
||||
position: 'bottom',
|
||||
offsetX: 20,
|
||||
labels: {
|
||||
useSeriesColors: false
|
||||
},
|
||||
markers: {
|
||||
width: 16,
|
||||
height: 16,
|
||||
radius: 5
|
||||
},
|
||||
itemMargin: {
|
||||
horizontal: 15,
|
||||
vertical: 8
|
||||
}
|
||||
},
|
||||
fill: {
|
||||
type: 'solid'
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
grid: {
|
||||
show: true
|
||||
},
|
||||
tooltip: {
|
||||
theme: 'dark',
|
||||
fixed: {
|
||||
enabled: false
|
||||
},
|
||||
y: {
|
||||
formatter: function (val) {
|
||||
return '$' + val;
|
||||
}
|
||||
},
|
||||
marker: {
|
||||
show: false
|
||||
}
|
||||
}
|
||||
},
|
||||
series: []
|
||||
};
|
||||
99
web/berry/src/views/Dashboard/component/StatisticalCard.js
Normal file
99
web/berry/src/views/Dashboard/component/StatisticalCard.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// material-ui
|
||||
import { styled, useTheme } from '@mui/material/styles';
|
||||
import { Avatar, Box, List, ListItem, ListItemAvatar, ListItemText, Typography } from '@mui/material';
|
||||
|
||||
// project imports
|
||||
import MainCard from 'ui-component/cards/MainCard';
|
||||
import TotalIncomeCard from 'ui-component/cards/Skeleton/TotalIncomeCard';
|
||||
|
||||
// assets
|
||||
import TableChartOutlinedIcon from '@mui/icons-material/TableChartOutlined';
|
||||
|
||||
// styles
|
||||
const CardWrapper = styled(MainCard)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.primary.dark,
|
||||
color: theme.palette.primary.light,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
'&:after': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
width: 210,
|
||||
height: 210,
|
||||
background: `linear-gradient(210.04deg, ${theme.palette.primary[200]} -50.94%, rgba(144, 202, 249, 0) 83.49%)`,
|
||||
borderRadius: '50%',
|
||||
top: -30,
|
||||
right: -180
|
||||
},
|
||||
'&:before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
width: 210,
|
||||
height: 210,
|
||||
background: `linear-gradient(140.9deg, ${theme.palette.primary[200]} -14.02%, rgba(144, 202, 249, 0) 77.58%)`,
|
||||
borderRadius: '50%',
|
||||
top: -160,
|
||||
right: -130
|
||||
}
|
||||
}));
|
||||
|
||||
// ==============================|| DASHBOARD - TOTAL INCOME DARK CARD ||============================== //
|
||||
|
||||
const StatisticalCard = ({ isLoading }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<TotalIncomeCard />
|
||||
) : (
|
||||
<CardWrapper border={false} content={false}>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<List sx={{ py: 0 }}>
|
||||
<ListItem alignItems="center" disableGutters sx={{ py: 0 }}>
|
||||
<ListItemAvatar>
|
||||
<Avatar
|
||||
variant="rounded"
|
||||
sx={{
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.largeAvatar,
|
||||
backgroundColor: theme.palette.primary[800],
|
||||
color: '#fff'
|
||||
}}
|
||||
>
|
||||
<TableChartOutlinedIcon fontSize="inherit" />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
sx={{
|
||||
py: 0,
|
||||
mt: 0.45,
|
||||
mb: 0.45
|
||||
}}
|
||||
primary={
|
||||
<Typography variant="h4" sx={{ color: '#fff' }}>
|
||||
$203k
|
||||
</Typography>
|
||||
}
|
||||
secondary={
|
||||
<Typography variant="subtitle2" sx={{ color: 'primary.light', mt: 0.25 }}>
|
||||
Total Income
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Box>
|
||||
</CardWrapper>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
StatisticalCard.propTypes = {
|
||||
isLoading: PropTypes.bool
|
||||
};
|
||||
|
||||
export default StatisticalCard;
|
||||
@@ -0,0 +1,122 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// material-ui
|
||||
import { useTheme, styled } from '@mui/material/styles';
|
||||
import { Box, Grid, Typography } from '@mui/material';
|
||||
|
||||
// third-party
|
||||
import Chart from 'react-apexcharts';
|
||||
|
||||
// project imports
|
||||
import MainCard from 'ui-component/cards/MainCard';
|
||||
import SkeletonTotalOrderCard from 'ui-component/cards/Skeleton/EarningCard';
|
||||
|
||||
const CardWrapper = styled(MainCard)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.primary.dark,
|
||||
color: '#fff',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
'&>div': {
|
||||
position: 'relative',
|
||||
zIndex: 5
|
||||
},
|
||||
'&:after': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
width: 210,
|
||||
height: 210,
|
||||
background: theme.palette.primary[800],
|
||||
borderRadius: '50%',
|
||||
zIndex: 1,
|
||||
top: -85,
|
||||
right: -95,
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
top: -105,
|
||||
right: -140
|
||||
}
|
||||
},
|
||||
'&:before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
width: 210,
|
||||
height: 210,
|
||||
background: theme.palette.primary[800],
|
||||
borderRadius: '50%',
|
||||
top: -125,
|
||||
right: -15,
|
||||
opacity: 0.5,
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
top: -155,
|
||||
right: -70
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// ==============================|| DASHBOARD - TOTAL ORDER LINE CHART CARD ||============================== //
|
||||
|
||||
const StatisticalLineChartCard = ({ isLoading, title, chartData, todayValue }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<SkeletonTotalOrderCard />
|
||||
) : (
|
||||
<CardWrapper border={false} content={false}>
|
||||
<Box sx={{ p: 2.25 }}>
|
||||
<Grid container direction="column">
|
||||
<Grid item sx={{ mb: 0.75 }}>
|
||||
<Grid container alignItems="center">
|
||||
<Grid item xs={6}>
|
||||
<Grid container alignItems="center">
|
||||
<Grid item>
|
||||
<Typography sx={{ fontSize: '2.125rem', fontWeight: 500, mr: 1, mt: 1.75, mb: 0.75 }}>
|
||||
{todayValue || '0'}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item></Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '1rem',
|
||||
fontWeight: 500,
|
||||
color: theme.palette.primary[200]
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
{chartData ? (
|
||||
<Chart {...chartData} />
|
||||
) : (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '1rem',
|
||||
fontWeight: 500,
|
||||
color: theme.palette.primary[200]
|
||||
}}
|
||||
>
|
||||
无数据
|
||||
</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</CardWrapper>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
StatisticalLineChartCard.propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
title: PropTypes.string
|
||||
};
|
||||
|
||||
export default StatisticalLineChartCard;
|
||||
218
web/berry/src/views/Dashboard/index.js
Normal file
218
web/berry/src/views/Dashboard/index.js
Normal file
@@ -0,0 +1,218 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Grid, Typography } from '@mui/material';
|
||||
import { gridSpacing } from 'store/constant';
|
||||
import StatisticalLineChartCard from './component/StatisticalLineChartCard';
|
||||
import StatisticalBarChart from './component/StatisticalBarChart';
|
||||
import { generateChartOptions, getLastSevenDays } from 'utils/chart';
|
||||
import { API } from 'utils/api';
|
||||
import { showError, calculateQuota, renderNumber } from 'utils/common';
|
||||
import UserCard from 'ui-component/cards/UserCard';
|
||||
|
||||
const Dashboard = () => {
|
||||
const [isLoading, setLoading] = useState(true);
|
||||
const [statisticalData, setStatisticalData] = useState([]);
|
||||
const [requestChart, setRequestChart] = useState(null);
|
||||
const [quotaChart, setQuotaChart] = useState(null);
|
||||
const [tokenChart, setTokenChart] = useState(null);
|
||||
const [users, setUsers] = useState([]);
|
||||
|
||||
const userDashboard = async () => {
|
||||
const res = await API.get('/api/user/dashboard');
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
if (data) {
|
||||
let lineData = getLineDataGroup(data);
|
||||
setRequestChart(getLineCardOption(lineData, 'RequestCount'));
|
||||
setQuotaChart(getLineCardOption(lineData, 'Quota'));
|
||||
setTokenChart(getLineCardOption(lineData, 'PromptTokens'));
|
||||
setStatisticalData(getBarDataGroup(data));
|
||||
}
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const loadUser = async () => {
|
||||
let res = await API.get(`/api/user/self`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setUsers(data);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
userDashboard();
|
||||
loadUser();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Grid container spacing={gridSpacing}>
|
||||
<Grid item xs={12}>
|
||||
<Grid container spacing={gridSpacing}>
|
||||
<Grid item lg={4} xs={12}>
|
||||
<StatisticalLineChartCard
|
||||
isLoading={isLoading}
|
||||
title="今日请求量"
|
||||
chartData={requestChart?.chartData}
|
||||
todayValue={requestChart?.todayValue}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item lg={4} xs={12}>
|
||||
<StatisticalLineChartCard
|
||||
isLoading={isLoading}
|
||||
title="今日消费"
|
||||
chartData={quotaChart?.chartData}
|
||||
todayValue={quotaChart?.todayValue}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item lg={4} xs={12}>
|
||||
<StatisticalLineChartCard
|
||||
isLoading={isLoading}
|
||||
title="今日 token"
|
||||
chartData={tokenChart?.chartData}
|
||||
todayValue={tokenChart?.todayValue}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Grid container spacing={gridSpacing}>
|
||||
<Grid item lg={8} xs={12}>
|
||||
<StatisticalBarChart isLoading={isLoading} chartDatas={statisticalData} />
|
||||
</Grid>
|
||||
<Grid item lg={4} xs={12}>
|
||||
<UserCard>
|
||||
<Grid container spacing={gridSpacing} justifyContent="center" alignItems="center" paddingTop={'20px'}>
|
||||
<Grid item xs={4}>
|
||||
<Typography variant="h4">余额:</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={8}>
|
||||
<Typography variant="h3"> {users?.quota ? '$' + calculateQuota(users.quota) : '未知'}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Typography variant="h4">已使用:</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={8}>
|
||||
<Typography variant="h3"> {users?.used_quota ? '$' + calculateQuota(users.used_quota) : '未知'}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Typography variant="h4">调用次数:</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={8}>
|
||||
<Typography variant="h3"> {users?.request_count || '未知'}</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
export default Dashboard;
|
||||
|
||||
function getLineDataGroup(statisticalData) {
|
||||
let groupedData = statisticalData.reduce((acc, cur) => {
|
||||
if (!acc[cur.Day]) {
|
||||
acc[cur.Day] = {
|
||||
date: cur.Day,
|
||||
RequestCount: 0,
|
||||
Quota: 0,
|
||||
PromptTokens: 0,
|
||||
CompletionTokens: 0
|
||||
};
|
||||
}
|
||||
acc[cur.Day].RequestCount += cur.RequestCount;
|
||||
acc[cur.Day].Quota += cur.Quota;
|
||||
acc[cur.Day].PromptTokens += cur.PromptTokens;
|
||||
acc[cur.Day].CompletionTokens += cur.CompletionTokens;
|
||||
return acc;
|
||||
}, {});
|
||||
let lastSevenDays = getLastSevenDays();
|
||||
return lastSevenDays.map((day) => {
|
||||
if (!groupedData[day]) {
|
||||
return {
|
||||
date: day,
|
||||
RequestCount: 0,
|
||||
Quota: 0,
|
||||
PromptTokens: 0,
|
||||
CompletionTokens: 0
|
||||
};
|
||||
} else {
|
||||
return groupedData[day];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getBarDataGroup(data) {
|
||||
const lastSevenDays = getLastSevenDays();
|
||||
const result = [];
|
||||
const map = new Map();
|
||||
|
||||
for (const item of data) {
|
||||
if (!map.has(item.ModelName)) {
|
||||
const newData = { name: item.ModelName, data: new Array(7) };
|
||||
map.set(item.ModelName, newData);
|
||||
result.push(newData);
|
||||
}
|
||||
const index = lastSevenDays.indexOf(item.Day);
|
||||
if (index !== -1) {
|
||||
map.get(item.ModelName).data[index] = calculateQuota(item.Quota, 3);
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of result) {
|
||||
for (let i = 0; i < 7; i++) {
|
||||
if (item.data[i] === undefined) {
|
||||
item.data[i] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { data: result, xaxis: lastSevenDays };
|
||||
}
|
||||
|
||||
function getLineCardOption(lineDataGroup, field) {
|
||||
let todayValue = 0;
|
||||
let chartData = null;
|
||||
const lastItem = lineDataGroup.length - 1;
|
||||
let lineData = lineDataGroup.map((item, index) => {
|
||||
let tmp = {
|
||||
date: item.date,
|
||||
value: item[field]
|
||||
};
|
||||
switch (field) {
|
||||
case 'Quota':
|
||||
tmp.value = calculateQuota(item.Quota, 3);
|
||||
break;
|
||||
case 'PromptTokens':
|
||||
tmp.value += item.CompletionTokens;
|
||||
break;
|
||||
}
|
||||
|
||||
if (index == lastItem) {
|
||||
todayValue = tmp.value;
|
||||
}
|
||||
return tmp;
|
||||
});
|
||||
|
||||
switch (field) {
|
||||
case 'RequestCount':
|
||||
chartData = generateChartOptions(lineData, '次');
|
||||
todayValue = renderNumber(todayValue);
|
||||
break;
|
||||
case 'Quota':
|
||||
chartData = generateChartOptions(lineData, '美元');
|
||||
todayValue = '$' + renderNumber(todayValue);
|
||||
break;
|
||||
case 'PromptTokens':
|
||||
chartData = generateChartOptions(lineData, '');
|
||||
todayValue = renderNumber(todayValue);
|
||||
break;
|
||||
}
|
||||
|
||||
return { chartData: chartData, todayValue: todayValue };
|
||||
}
|
||||
Reference in New Issue
Block a user