初始化导入 new-api 源码
This commit is contained in:
126
web/src/components/dashboard/AnnouncementsPanel.jsx
Normal file
126
web/src/components/dashboard/AnnouncementsPanel.jsx
Normal file
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Card, Tag, Timeline, Empty } from '@douyinfe/semi-ui';
|
||||
import { Bell } from 'lucide-react';
|
||||
import { marked } from 'marked';
|
||||
import {
|
||||
IllustrationConstruction,
|
||||
IllustrationConstructionDark,
|
||||
} from '@douyinfe/semi-illustrations';
|
||||
import ScrollableContainer from '../common/ui/ScrollableContainer';
|
||||
|
||||
const AnnouncementsPanel = ({
|
||||
announcementData,
|
||||
announcementLegendData,
|
||||
CARD_PROPS,
|
||||
ILLUSTRATION_SIZE,
|
||||
t,
|
||||
}) => {
|
||||
return (
|
||||
<Card
|
||||
{...CARD_PROPS}
|
||||
className='shadow-sm !rounded-2xl lg:col-span-2'
|
||||
title={
|
||||
<div className='flex flex-col lg:flex-row lg:items-center lg:justify-between gap-2 w-full'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Bell size={16} />
|
||||
{t('系统公告')}
|
||||
<Tag color='white' shape='circle'>
|
||||
{t('显示最新20条')}
|
||||
</Tag>
|
||||
</div>
|
||||
{/* 图例 */}
|
||||
<div className='flex flex-wrap gap-3 text-xs'>
|
||||
{announcementLegendData.map((legend, index) => (
|
||||
<div key={index} className='flex items-center gap-1'>
|
||||
<div
|
||||
className='w-2 h-2 rounded-full'
|
||||
style={{
|
||||
backgroundColor:
|
||||
legend.color === 'grey'
|
||||
? '#8b9aa7'
|
||||
: legend.color === 'blue'
|
||||
? '#3b82f6'
|
||||
: legend.color === 'green'
|
||||
? '#10b981'
|
||||
: legend.color === 'orange'
|
||||
? '#f59e0b'
|
||||
: legend.color === 'red'
|
||||
? '#ef4444'
|
||||
: '#8b9aa7',
|
||||
}}
|
||||
/>
|
||||
<span className='text-gray-600'>{legend.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
<ScrollableContainer maxHeight='24rem'>
|
||||
{announcementData.length > 0 ? (
|
||||
<Timeline mode='left'>
|
||||
{announcementData.map((item, idx) => {
|
||||
const htmlExtra = item.extra ? marked.parse(item.extra) : '';
|
||||
return (
|
||||
<Timeline.Item
|
||||
key={idx}
|
||||
type={item.type || 'default'}
|
||||
time={`${item.relative ? item.relative + ' ' : ''}${item.time}`}
|
||||
extra={
|
||||
item.extra ? (
|
||||
<div
|
||||
className='text-xs text-gray-500'
|
||||
dangerouslySetInnerHTML={{ __html: htmlExtra }}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: marked.parse(item.content || ''),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Timeline.Item>
|
||||
);
|
||||
})}
|
||||
</Timeline>
|
||||
) : (
|
||||
<div className='flex justify-center items-center py-8'>
|
||||
<Empty
|
||||
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
|
||||
darkModeImage={
|
||||
<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />
|
||||
}
|
||||
title={t('暂无系统公告')}
|
||||
description={t('请联系管理员在系统设置中配置公告信息')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</ScrollableContainer>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnnouncementsPanel;
|
||||
119
web/src/components/dashboard/ApiInfoPanel.jsx
Normal file
119
web/src/components/dashboard/ApiInfoPanel.jsx
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Card, Avatar, Tag, Divider, Empty } from '@douyinfe/semi-ui';
|
||||
import { Server, Gauge, ExternalLink } from 'lucide-react';
|
||||
import {
|
||||
IllustrationConstruction,
|
||||
IllustrationConstructionDark,
|
||||
} from '@douyinfe/semi-illustrations';
|
||||
import ScrollableContainer from '../common/ui/ScrollableContainer';
|
||||
|
||||
const ApiInfoPanel = ({
|
||||
apiInfoData,
|
||||
handleCopyUrl,
|
||||
handleSpeedTest,
|
||||
CARD_PROPS,
|
||||
FLEX_CENTER_GAP2,
|
||||
ILLUSTRATION_SIZE,
|
||||
t,
|
||||
}) => {
|
||||
return (
|
||||
<Card
|
||||
{...CARD_PROPS}
|
||||
className='bg-gray-50 border-0 !rounded-2xl'
|
||||
title={
|
||||
<div className={FLEX_CENTER_GAP2}>
|
||||
<Server size={16} />
|
||||
{t('API信息')}
|
||||
</div>
|
||||
}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
<ScrollableContainer maxHeight='24rem'>
|
||||
{apiInfoData.length > 0 ? (
|
||||
apiInfoData.map((api) => (
|
||||
<React.Fragment key={api.id}>
|
||||
<div className='flex p-2 hover:bg-white rounded-lg transition-colors cursor-pointer'>
|
||||
<div className='flex-shrink-0 mr-3'>
|
||||
<Avatar size='extra-small' color={api.color}>
|
||||
{api.route.substring(0, 2)}
|
||||
</Avatar>
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<div className='flex flex-wrap items-center justify-between mb-1 w-full gap-2'>
|
||||
<span className='text-sm font-medium text-gray-900 !font-bold break-all'>
|
||||
{api.route}
|
||||
</span>
|
||||
<div className='flex items-center gap-1 mt-1 lg:mt-0'>
|
||||
<Tag
|
||||
prefixIcon={<Gauge size={12} />}
|
||||
size='small'
|
||||
color='white'
|
||||
shape='circle'
|
||||
onClick={() => handleSpeedTest(api.url)}
|
||||
className='cursor-pointer hover:opacity-80 text-xs'
|
||||
>
|
||||
{t('测速')}
|
||||
</Tag>
|
||||
<Tag
|
||||
prefixIcon={<ExternalLink size={12} />}
|
||||
size='small'
|
||||
color='white'
|
||||
shape='circle'
|
||||
onClick={() =>
|
||||
window.open(api.url, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
className='cursor-pointer hover:opacity-80 text-xs'
|
||||
>
|
||||
{t('跳转')}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className='!text-semi-color-primary break-all cursor-pointer hover:underline mb-1'
|
||||
onClick={() => handleCopyUrl(api.url)}
|
||||
>
|
||||
{api.url}
|
||||
</div>
|
||||
<div className='text-gray-500'>{api.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
</React.Fragment>
|
||||
))
|
||||
) : (
|
||||
<div className='flex justify-center items-center min-h-[20rem] w-full'>
|
||||
<Empty
|
||||
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
|
||||
darkModeImage={
|
||||
<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />
|
||||
}
|
||||
title={t('暂无API信息')}
|
||||
description={t('请联系管理员在系统设置中配置API信息')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</ScrollableContainer>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiInfoPanel;
|
||||
80
web/src/components/dashboard/ChartsPanel.jsx
Normal file
80
web/src/components/dashboard/ChartsPanel.jsx
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Card, Tabs, TabPane } from '@douyinfe/semi-ui';
|
||||
import { PieChart } from 'lucide-react';
|
||||
import { VChart } from '@visactor/react-vchart';
|
||||
|
||||
const ChartsPanel = ({
|
||||
activeChartTab,
|
||||
setActiveChartTab,
|
||||
spec_line,
|
||||
spec_model_line,
|
||||
spec_pie,
|
||||
spec_rank_bar,
|
||||
CARD_PROPS,
|
||||
CHART_CONFIG,
|
||||
FLEX_CENTER_GAP2,
|
||||
hasApiInfoPanel,
|
||||
t,
|
||||
}) => {
|
||||
return (
|
||||
<Card
|
||||
{...CARD_PROPS}
|
||||
className={`!rounded-2xl ${hasApiInfoPanel ? 'lg:col-span-3' : ''}`}
|
||||
title={
|
||||
<div className='flex flex-col lg:flex-row lg:items-center lg:justify-between w-full gap-3'>
|
||||
<div className={FLEX_CENTER_GAP2}>
|
||||
<PieChart size={16} />
|
||||
{t('模型数据分析')}
|
||||
</div>
|
||||
<Tabs
|
||||
type='slash'
|
||||
activeKey={activeChartTab}
|
||||
onChange={setActiveChartTab}
|
||||
>
|
||||
<TabPane tab={<span>{t('消耗分布')}</span>} itemKey='1' />
|
||||
<TabPane tab={<span>{t('消耗趋势')}</span>} itemKey='2' />
|
||||
<TabPane tab={<span>{t('调用次数分布')}</span>} itemKey='3' />
|
||||
<TabPane tab={<span>{t('调用次数排行')}</span>} itemKey='4' />
|
||||
</Tabs>
|
||||
</div>
|
||||
}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
<div className='h-96 p-2'>
|
||||
{activeChartTab === '1' && (
|
||||
<VChart spec={spec_line} option={CHART_CONFIG} />
|
||||
)}
|
||||
{activeChartTab === '2' && (
|
||||
<VChart spec={spec_model_line} option={CHART_CONFIG} />
|
||||
)}
|
||||
{activeChartTab === '3' && (
|
||||
<VChart spec={spec_pie} option={CHART_CONFIG} />
|
||||
)}
|
||||
{activeChartTab === '4' && (
|
||||
<VChart spec={spec_rank_bar} option={CHART_CONFIG} />
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChartsPanel;
|
||||
61
web/src/components/dashboard/DashboardHeader.jsx
Normal file
61
web/src/components/dashboard/DashboardHeader.jsx
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Button } from '@douyinfe/semi-ui';
|
||||
import { RefreshCw, Search } from 'lucide-react';
|
||||
|
||||
const DashboardHeader = ({
|
||||
getGreeting,
|
||||
greetingVisible,
|
||||
showSearchModal,
|
||||
refresh,
|
||||
loading,
|
||||
t,
|
||||
}) => {
|
||||
const ICON_BUTTON_CLASS = 'text-white hover:bg-opacity-80 !rounded-full';
|
||||
|
||||
return (
|
||||
<div className='flex items-center justify-between mb-4'>
|
||||
<h2
|
||||
className='text-2xl font-semibold text-gray-800 transition-opacity duration-1000 ease-in-out'
|
||||
style={{ opacity: greetingVisible ? 1 : 0 }}
|
||||
>
|
||||
{getGreeting}
|
||||
</h2>
|
||||
<div className='flex gap-3'>
|
||||
<Button
|
||||
type='tertiary'
|
||||
icon={<Search size={16} />}
|
||||
onClick={showSearchModal}
|
||||
className={`bg-green-500 hover:bg-green-600 ${ICON_BUTTON_CLASS}`}
|
||||
/>
|
||||
<Button
|
||||
type='tertiary'
|
||||
icon={<RefreshCw size={16} />}
|
||||
onClick={refresh}
|
||||
loading={loading}
|
||||
className={`bg-blue-500 hover:bg-blue-600 ${ICON_BUTTON_CLASS}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardHeader;
|
||||
88
web/src/components/dashboard/FaqPanel.jsx
Normal file
88
web/src/components/dashboard/FaqPanel.jsx
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Card, Collapse, Empty } from '@douyinfe/semi-ui';
|
||||
import { HelpCircle } from 'lucide-react';
|
||||
import { IconPlus, IconMinus } from '@douyinfe/semi-icons';
|
||||
import { marked } from 'marked';
|
||||
import {
|
||||
IllustrationConstruction,
|
||||
IllustrationConstructionDark,
|
||||
} from '@douyinfe/semi-illustrations';
|
||||
import ScrollableContainer from '../common/ui/ScrollableContainer';
|
||||
|
||||
const FaqPanel = ({
|
||||
faqData,
|
||||
CARD_PROPS,
|
||||
FLEX_CENTER_GAP2,
|
||||
ILLUSTRATION_SIZE,
|
||||
t,
|
||||
}) => {
|
||||
return (
|
||||
<Card
|
||||
{...CARD_PROPS}
|
||||
className='shadow-sm !rounded-2xl lg:col-span-1'
|
||||
title={
|
||||
<div className={FLEX_CENTER_GAP2}>
|
||||
<HelpCircle size={16} />
|
||||
{t('常见问答')}
|
||||
</div>
|
||||
}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
<ScrollableContainer maxHeight='24rem'>
|
||||
{faqData.length > 0 ? (
|
||||
<Collapse
|
||||
accordion
|
||||
expandIcon={<IconPlus />}
|
||||
collapseIcon={<IconMinus />}
|
||||
>
|
||||
{faqData.map((item, index) => (
|
||||
<Collapse.Panel
|
||||
key={index}
|
||||
header={item.question}
|
||||
itemKey={index.toString()}
|
||||
>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: marked.parse(item.answer || ''),
|
||||
}}
|
||||
/>
|
||||
</Collapse.Panel>
|
||||
))}
|
||||
</Collapse>
|
||||
) : (
|
||||
<div className='flex justify-center items-center py-8'>
|
||||
<Empty
|
||||
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
|
||||
darkModeImage={
|
||||
<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />
|
||||
}
|
||||
title={t('暂无常见问答')}
|
||||
description={t('请联系管理员在系统设置中配置常见问答')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</ScrollableContainer>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default FaqPanel;
|
||||
116
web/src/components/dashboard/StatsCards.jsx
Normal file
116
web/src/components/dashboard/StatsCards.jsx
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Card, Avatar, Skeleton, Tag } from '@douyinfe/semi-ui';
|
||||
import { VChart } from '@visactor/react-vchart';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const StatsCards = ({
|
||||
groupedStatsData,
|
||||
loading,
|
||||
getTrendSpec,
|
||||
CARD_PROPS,
|
||||
CHART_CONFIG,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className='mb-4'>
|
||||
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4'>
|
||||
{groupedStatsData.map((group, idx) => (
|
||||
<Card
|
||||
key={idx}
|
||||
{...CARD_PROPS}
|
||||
className={`${group.color} border-0 !rounded-2xl w-full`}
|
||||
title={group.title}
|
||||
>
|
||||
<div className='space-y-4'>
|
||||
{group.items.map((item, itemIdx) => (
|
||||
<div
|
||||
key={itemIdx}
|
||||
className='flex items-center justify-between cursor-pointer'
|
||||
onClick={item.onClick}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<Avatar
|
||||
className='mr-3'
|
||||
size='small'
|
||||
color={item.avatarColor}
|
||||
>
|
||||
{item.icon}
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className='text-xs text-gray-500'>{item.title}</div>
|
||||
<div className='text-lg font-semibold'>
|
||||
<Skeleton
|
||||
loading={loading}
|
||||
active
|
||||
placeholder={
|
||||
<Skeleton.Paragraph
|
||||
active
|
||||
rows={1}
|
||||
style={{
|
||||
width: '65px',
|
||||
height: '24px',
|
||||
marginTop: '4px',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{item.value}
|
||||
</Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{item.title === t('当前余额') ? (
|
||||
<Tag
|
||||
color='white'
|
||||
shape='circle'
|
||||
size='large'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate('/console/topup');
|
||||
}}
|
||||
>
|
||||
{t('充值')}
|
||||
</Tag>
|
||||
) : (
|
||||
(loading ||
|
||||
(item.trendData && item.trendData.length > 0)) && (
|
||||
<div className='w-24 h-10'>
|
||||
<VChart
|
||||
spec={getTrendSpec(item.trendData, item.trendColor)}
|
||||
option={CHART_CONFIG}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatsCards;
|
||||
152
web/src/components/dashboard/UptimePanel.jsx
Normal file
152
web/src/components/dashboard/UptimePanel.jsx
Normal file
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
Button,
|
||||
Spin,
|
||||
Tabs,
|
||||
TabPane,
|
||||
Tag,
|
||||
Empty,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { Gauge, RefreshCw } from 'lucide-react';
|
||||
import {
|
||||
IllustrationConstruction,
|
||||
IllustrationConstructionDark,
|
||||
} from '@douyinfe/semi-illustrations';
|
||||
import ScrollableContainer from '../common/ui/ScrollableContainer';
|
||||
|
||||
const UptimePanel = ({
|
||||
uptimeData,
|
||||
uptimeLoading,
|
||||
activeUptimeTab,
|
||||
setActiveUptimeTab,
|
||||
loadUptimeData,
|
||||
uptimeLegendData,
|
||||
renderMonitorList,
|
||||
CARD_PROPS,
|
||||
ILLUSTRATION_SIZE,
|
||||
t,
|
||||
}) => {
|
||||
return (
|
||||
<Card
|
||||
{...CARD_PROPS}
|
||||
className='shadow-sm !rounded-2xl lg:col-span-1'
|
||||
title={
|
||||
<div className='flex items-center justify-between w-full gap-2'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Gauge size={16} />
|
||||
{t('服务可用性')}
|
||||
</div>
|
||||
<Button
|
||||
icon={<RefreshCw size={14} />}
|
||||
onClick={loadUptimeData}
|
||||
loading={uptimeLoading}
|
||||
size='small'
|
||||
theme='borderless'
|
||||
type='tertiary'
|
||||
className='text-gray-500 hover:text-blue-500 hover:bg-blue-50 !rounded-full'
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
{/* 内容区域 */}
|
||||
<div className='relative'>
|
||||
<Spin spinning={uptimeLoading}>
|
||||
{uptimeData.length > 0 ? (
|
||||
uptimeData.length === 1 ? (
|
||||
<ScrollableContainer maxHeight='24rem'>
|
||||
{renderMonitorList(uptimeData[0].monitors)}
|
||||
</ScrollableContainer>
|
||||
) : (
|
||||
<Tabs
|
||||
type='card'
|
||||
collapsible
|
||||
activeKey={activeUptimeTab}
|
||||
onChange={setActiveUptimeTab}
|
||||
size='small'
|
||||
>
|
||||
{uptimeData.map((group, groupIdx) => (
|
||||
<TabPane
|
||||
tab={
|
||||
<span className='flex items-center gap-2'>
|
||||
<Gauge size={14} />
|
||||
{group.categoryName}
|
||||
<Tag
|
||||
color={
|
||||
activeUptimeTab === group.categoryName
|
||||
? 'red'
|
||||
: 'grey'
|
||||
}
|
||||
size='small'
|
||||
shape='circle'
|
||||
>
|
||||
{group.monitors ? group.monitors.length : 0}
|
||||
</Tag>
|
||||
</span>
|
||||
}
|
||||
itemKey={group.categoryName}
|
||||
key={groupIdx}
|
||||
>
|
||||
<ScrollableContainer maxHeight='21.5rem'>
|
||||
{renderMonitorList(group.monitors)}
|
||||
</ScrollableContainer>
|
||||
</TabPane>
|
||||
))}
|
||||
</Tabs>
|
||||
)
|
||||
) : (
|
||||
<div className='flex justify-center items-center py-8'>
|
||||
<Empty
|
||||
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
|
||||
darkModeImage={
|
||||
<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />
|
||||
}
|
||||
title={t('暂无监控数据')}
|
||||
description={t('请联系管理员在系统设置中配置Uptime')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Spin>
|
||||
</div>
|
||||
|
||||
{/* 图例 */}
|
||||
{uptimeData.length > 0 && (
|
||||
<div className='p-3 bg-gray-50 rounded-b-2xl'>
|
||||
<div className='flex flex-wrap gap-3 text-xs justify-center'>
|
||||
{uptimeLegendData.map((legend, index) => (
|
||||
<div key={index} className='flex items-center gap-1'>
|
||||
<div
|
||||
className='w-2 h-2 rounded-full'
|
||||
style={{ backgroundColor: legend.color }}
|
||||
/>
|
||||
<span className='text-gray-600'>{legend.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default UptimePanel;
|
||||
273
web/src/components/dashboard/index.jsx
Normal file
273
web/src/components/dashboard/index.jsx
Normal file
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { getRelativeTime } from '../../helpers';
|
||||
import { UserContext } from '../../context/User';
|
||||
import { StatusContext } from '../../context/Status';
|
||||
|
||||
import DashboardHeader from './DashboardHeader';
|
||||
import StatsCards from './StatsCards';
|
||||
import ChartsPanel from './ChartsPanel';
|
||||
import ApiInfoPanel from './ApiInfoPanel';
|
||||
import AnnouncementsPanel from './AnnouncementsPanel';
|
||||
import FaqPanel from './FaqPanel';
|
||||
import UptimePanel from './UptimePanel';
|
||||
import SearchModal from './modals/SearchModal';
|
||||
|
||||
import { useDashboardData } from '../../hooks/dashboard/useDashboardData';
|
||||
import { useDashboardStats } from '../../hooks/dashboard/useDashboardStats';
|
||||
import { useDashboardCharts } from '../../hooks/dashboard/useDashboardCharts';
|
||||
|
||||
import {
|
||||
CHART_CONFIG,
|
||||
CARD_PROPS,
|
||||
FLEX_CENTER_GAP2,
|
||||
ILLUSTRATION_SIZE,
|
||||
ANNOUNCEMENT_LEGEND_DATA,
|
||||
UPTIME_STATUS_MAP,
|
||||
} from '../../constants/dashboard.constants';
|
||||
import {
|
||||
getTrendSpec,
|
||||
handleCopyUrl,
|
||||
handleSpeedTest,
|
||||
getUptimeStatusColor,
|
||||
getUptimeStatusText,
|
||||
renderMonitorList,
|
||||
} from '../../helpers/dashboard';
|
||||
|
||||
const Dashboard = () => {
|
||||
// ========== Context ==========
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
const [statusState, statusDispatch] = useContext(StatusContext);
|
||||
|
||||
// ========== 主要数据管理 ==========
|
||||
const dashboardData = useDashboardData(userState, userDispatch, statusState);
|
||||
|
||||
// ========== 图表管理 ==========
|
||||
const dashboardCharts = useDashboardCharts(
|
||||
dashboardData.dataExportDefaultTime,
|
||||
dashboardData.setTrendData,
|
||||
dashboardData.setConsumeQuota,
|
||||
dashboardData.setTimes,
|
||||
dashboardData.setConsumeTokens,
|
||||
dashboardData.setPieData,
|
||||
dashboardData.setLineData,
|
||||
dashboardData.setModelColors,
|
||||
dashboardData.t,
|
||||
);
|
||||
|
||||
// ========== 统计数据 ==========
|
||||
const { groupedStatsData } = useDashboardStats(
|
||||
userState,
|
||||
dashboardData.consumeQuota,
|
||||
dashboardData.consumeTokens,
|
||||
dashboardData.times,
|
||||
dashboardData.trendData,
|
||||
dashboardData.performanceMetrics,
|
||||
dashboardData.cacheMetrics,
|
||||
dashboardData.navigate,
|
||||
dashboardData.t,
|
||||
);
|
||||
|
||||
// ========== 数据处理 ==========
|
||||
const initChart = async () => {
|
||||
await dashboardData.loadQuotaData().then((data) => {
|
||||
if (data && data.length > 0) {
|
||||
dashboardCharts.updateChartData(data);
|
||||
}
|
||||
});
|
||||
await dashboardData.loadUptimeData();
|
||||
await dashboardData.loadCacheStats?.();
|
||||
};
|
||||
|
||||
const handleRefresh = async () => {
|
||||
const data = await dashboardData.refresh();
|
||||
if (data && data.length > 0) {
|
||||
dashboardCharts.updateChartData(data);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearchConfirm = async () => {
|
||||
await dashboardData.handleSearchConfirm(dashboardCharts.updateChartData);
|
||||
};
|
||||
|
||||
// ========== 数据准备 ==========
|
||||
const apiInfoData = statusState?.status?.api_info || [];
|
||||
const announcementData = (statusState?.status?.announcements || []).map(
|
||||
(item) => {
|
||||
const pubDate = item?.publishDate ? new Date(item.publishDate) : null;
|
||||
const absoluteTime =
|
||||
pubDate && !isNaN(pubDate.getTime())
|
||||
? `${pubDate.getFullYear()}-${String(pubDate.getMonth() + 1).padStart(2, '0')}-${String(pubDate.getDate()).padStart(2, '0')} ${String(pubDate.getHours()).padStart(2, '0')}:${String(pubDate.getMinutes()).padStart(2, '0')}`
|
||||
: item?.publishDate || '';
|
||||
const relativeTime = getRelativeTime(item.publishDate);
|
||||
return {
|
||||
...item,
|
||||
time: absoluteTime,
|
||||
relative: relativeTime,
|
||||
};
|
||||
},
|
||||
);
|
||||
const faqData = statusState?.status?.faq || [];
|
||||
|
||||
const uptimeLegendData = Object.entries(UPTIME_STATUS_MAP).map(
|
||||
([status, info]) => ({
|
||||
status: Number(status),
|
||||
color: info.color,
|
||||
label: dashboardData.t(info.label),
|
||||
}),
|
||||
);
|
||||
|
||||
// ========== Effects ==========
|
||||
useEffect(() => {
|
||||
initChart();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className='h-full'>
|
||||
<DashboardHeader
|
||||
getGreeting={dashboardData.getGreeting}
|
||||
greetingVisible={dashboardData.greetingVisible}
|
||||
showSearchModal={dashboardData.showSearchModal}
|
||||
refresh={handleRefresh}
|
||||
loading={dashboardData.loading}
|
||||
t={dashboardData.t}
|
||||
/>
|
||||
|
||||
<SearchModal
|
||||
searchModalVisible={dashboardData.searchModalVisible}
|
||||
handleSearchConfirm={handleSearchConfirm}
|
||||
handleCloseModal={dashboardData.handleCloseModal}
|
||||
isMobile={dashboardData.isMobile}
|
||||
isAdminUser={dashboardData.isAdminUser}
|
||||
inputs={dashboardData.inputs}
|
||||
dataExportDefaultTime={dashboardData.dataExportDefaultTime}
|
||||
timeOptions={dashboardData.timeOptions}
|
||||
handleInputChange={dashboardData.handleInputChange}
|
||||
t={dashboardData.t}
|
||||
/>
|
||||
|
||||
<StatsCards
|
||||
groupedStatsData={groupedStatsData}
|
||||
loading={dashboardData.loading}
|
||||
getTrendSpec={getTrendSpec}
|
||||
CARD_PROPS={CARD_PROPS}
|
||||
CHART_CONFIG={CHART_CONFIG}
|
||||
/>
|
||||
|
||||
{/* API信息和图表面板 */}
|
||||
<div className='mb-4'>
|
||||
<div
|
||||
className={`grid grid-cols-1 gap-4 ${dashboardData.hasApiInfoPanel ? 'lg:grid-cols-4' : ''}`}
|
||||
>
|
||||
<ChartsPanel
|
||||
activeChartTab={dashboardData.activeChartTab}
|
||||
setActiveChartTab={dashboardData.setActiveChartTab}
|
||||
spec_line={dashboardCharts.spec_line}
|
||||
spec_model_line={dashboardCharts.spec_model_line}
|
||||
spec_pie={dashboardCharts.spec_pie}
|
||||
spec_rank_bar={dashboardCharts.spec_rank_bar}
|
||||
CARD_PROPS={CARD_PROPS}
|
||||
CHART_CONFIG={CHART_CONFIG}
|
||||
FLEX_CENTER_GAP2={FLEX_CENTER_GAP2}
|
||||
hasApiInfoPanel={dashboardData.hasApiInfoPanel}
|
||||
t={dashboardData.t}
|
||||
/>
|
||||
|
||||
{dashboardData.hasApiInfoPanel && (
|
||||
<ApiInfoPanel
|
||||
apiInfoData={apiInfoData}
|
||||
handleCopyUrl={(url) => handleCopyUrl(url, dashboardData.t)}
|
||||
handleSpeedTest={handleSpeedTest}
|
||||
CARD_PROPS={CARD_PROPS}
|
||||
FLEX_CENTER_GAP2={FLEX_CENTER_GAP2}
|
||||
ILLUSTRATION_SIZE={ILLUSTRATION_SIZE}
|
||||
t={dashboardData.t}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 系统公告和常见问答卡片 */}
|
||||
{dashboardData.hasInfoPanels && (
|
||||
<div className='mb-4'>
|
||||
<div className='grid grid-cols-1 lg:grid-cols-4 gap-4'>
|
||||
{/* 公告卡片 */}
|
||||
{dashboardData.announcementsEnabled && (
|
||||
<AnnouncementsPanel
|
||||
announcementData={announcementData}
|
||||
announcementLegendData={ANNOUNCEMENT_LEGEND_DATA.map(
|
||||
(item) => ({
|
||||
...item,
|
||||
label: dashboardData.t(item.label),
|
||||
}),
|
||||
)}
|
||||
CARD_PROPS={CARD_PROPS}
|
||||
ILLUSTRATION_SIZE={ILLUSTRATION_SIZE}
|
||||
t={dashboardData.t}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 常见问答卡片 */}
|
||||
{dashboardData.faqEnabled && (
|
||||
<FaqPanel
|
||||
faqData={faqData}
|
||||
CARD_PROPS={CARD_PROPS}
|
||||
FLEX_CENTER_GAP2={FLEX_CENTER_GAP2}
|
||||
ILLUSTRATION_SIZE={ILLUSTRATION_SIZE}
|
||||
t={dashboardData.t}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 服务可用性卡片 */}
|
||||
{dashboardData.uptimeEnabled && (
|
||||
<UptimePanel
|
||||
uptimeData={dashboardData.uptimeData}
|
||||
uptimeLoading={dashboardData.uptimeLoading}
|
||||
activeUptimeTab={dashboardData.activeUptimeTab}
|
||||
setActiveUptimeTab={dashboardData.setActiveUptimeTab}
|
||||
loadUptimeData={dashboardData.loadUptimeData}
|
||||
uptimeLegendData={uptimeLegendData}
|
||||
renderMonitorList={(monitors) =>
|
||||
renderMonitorList(
|
||||
monitors,
|
||||
(status) => getUptimeStatusColor(status, UPTIME_STATUS_MAP),
|
||||
(status) =>
|
||||
getUptimeStatusText(
|
||||
status,
|
||||
UPTIME_STATUS_MAP,
|
||||
dashboardData.t,
|
||||
),
|
||||
dashboardData.t,
|
||||
)
|
||||
}
|
||||
CARD_PROPS={CARD_PROPS}
|
||||
ILLUSTRATION_SIZE={ILLUSTRATION_SIZE}
|
||||
t={dashboardData.t}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
103
web/src/components/dashboard/modals/SearchModal.jsx
Normal file
103
web/src/components/dashboard/modals/SearchModal.jsx
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
import { Modal, Form } from '@douyinfe/semi-ui';
|
||||
|
||||
const SearchModal = ({
|
||||
searchModalVisible,
|
||||
handleSearchConfirm,
|
||||
handleCloseModal,
|
||||
isMobile,
|
||||
isAdminUser,
|
||||
inputs,
|
||||
dataExportDefaultTime,
|
||||
timeOptions,
|
||||
handleInputChange,
|
||||
t,
|
||||
}) => {
|
||||
const formRef = useRef();
|
||||
|
||||
const FORM_FIELD_PROPS = {
|
||||
className: 'w-full mb-2 !rounded-lg',
|
||||
};
|
||||
|
||||
const createFormField = (Component, props) => (
|
||||
<Component {...FORM_FIELD_PROPS} {...props} />
|
||||
);
|
||||
|
||||
const { start_timestamp, end_timestamp, username } = inputs;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('搜索条件')}
|
||||
visible={searchModalVisible}
|
||||
onOk={handleSearchConfirm}
|
||||
onCancel={handleCloseModal}
|
||||
closeOnEsc={true}
|
||||
size={isMobile ? 'full-width' : 'small'}
|
||||
centered
|
||||
>
|
||||
<Form ref={formRef} layout='vertical' className='w-full'>
|
||||
{createFormField(Form.DatePicker, {
|
||||
field: 'start_timestamp',
|
||||
label: t('起始时间'),
|
||||
initValue: start_timestamp,
|
||||
value: start_timestamp,
|
||||
type: 'dateTime',
|
||||
name: 'start_timestamp',
|
||||
onChange: (value) => handleInputChange(value, 'start_timestamp'),
|
||||
})}
|
||||
|
||||
{createFormField(Form.DatePicker, {
|
||||
field: 'end_timestamp',
|
||||
label: t('结束时间'),
|
||||
initValue: end_timestamp,
|
||||
value: end_timestamp,
|
||||
type: 'dateTime',
|
||||
name: 'end_timestamp',
|
||||
onChange: (value) => handleInputChange(value, 'end_timestamp'),
|
||||
})}
|
||||
|
||||
{createFormField(Form.Select, {
|
||||
field: 'data_export_default_time',
|
||||
label: t('时间粒度'),
|
||||
initValue: dataExportDefaultTime,
|
||||
placeholder: t('时间粒度'),
|
||||
name: 'data_export_default_time',
|
||||
optionList: timeOptions,
|
||||
onChange: (value) =>
|
||||
handleInputChange(value, 'data_export_default_time'),
|
||||
})}
|
||||
|
||||
{isAdminUser &&
|
||||
createFormField(Form.Input, {
|
||||
field: 'username',
|
||||
label: t('用户名称'),
|
||||
value: username,
|
||||
placeholder: t('可选值'),
|
||||
name: 'username',
|
||||
onChange: (value) => handleInputChange(value, 'username'),
|
||||
})}
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchModal;
|
||||
Reference in New Issue
Block a user