- Switch Semi UI Timeline to mode="left" in: - web/src/components/layout/NoticeModal.jsx - web/src/components/dashboard/AnnouncementsPanel.jsx - Show both relative and absolute time in the `time` prop (e.g. "3 days ago 2025-02-18 10:30") - Move auxiliary description to the `extra` prop and remove duplicate rendering from content area - Keep original `extra` data intact; compute and pass: - `time`: absolute time (yyyy-MM-dd HH:mm) - `relative`: relative time (e.g., "3 days ago") - Update data assembly to expose `time` and `relative` without overwriting `extra`: - web/src/components/dashboard/index.jsx - No i18n changes; no linter errors introduced Why: Aligns Timeline layout across the app and clarifies time context by combining relative and absolute timestamps while preserving auxiliary notes via `extra`.
255 lines
8.7 KiB
JavaScript
255 lines
8.7 KiB
JavaScript
/*
|
|
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.navigate,
|
|
dashboardData.t
|
|
);
|
|
|
|
// ========== 数据处理 ==========
|
|
const initChart = async () => {
|
|
await dashboardData.loadQuotaData().then(data => {
|
|
if (data && data.length > 0) {
|
|
dashboardCharts.updateChartData(data);
|
|
}
|
|
});
|
|
await dashboardData.loadUptimeData();
|
|
};
|
|
|
|
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;
|