## Overview Refactored the monolithic dashboard page (~1200 lines) into a modular architecture following the project's global layout pattern. The main `Detail/index.js` is now simplified to match other page entry files like `Midjourney/index.js`. ## Changes Made ### 🏗️ Architecture Changes - **Before**: Single large file `pages/Detail/index.js` containing all dashboard logic - **After**: Modular structure with dedicated hooks, components, and helpers ### 📁 New Files Created - `hooks/dashboard/useDashboardData.js` - Core data management and API calls - `hooks/dashboard/useDashboardStats.js` - Statistics computation and memoization - `hooks/dashboard/useDashboardCharts.js` - Chart specifications and data processing - `constants/dashboard.constants.js` - UI config, time options, and chart defaults - `helpers/dashboard.js` - Utility functions for data processing and UI helpers - `components/dashboard/index.jsx` - Main dashboard component integrating all modules - `components/dashboard/modals/SearchModal.jsx` - Search modal component ### 🔧 Updated Files - `constants/index.js` - Added dashboard constants export - `helpers/index.js` - Added dashboard helpers export - `pages/Detail/index.js` - Simplified to minimal wrapper (~20 lines) ### 🐛 Bug Fixes - Fixed SearchModal DatePicker onChange to properly convert Date objects to timestamp strings - Added missing localStorage update for `data_export_default_time` persistence - Corrected data flow between search confirmation and chart updates - Ensured proper chart data refresh after search parameter changes ### ✨ Key Improvements - **Separation of Concerns**: Data, stats, and charts logic isolated into dedicated hooks - **Reusability**: Components and hooks can be easily reused across the application - **Maintainability**: Smaller, focused files easier to understand and modify - **Consistency**: Follows established project patterns for global folder organization - **Performance**: Proper memoization and callback optimization maintained ### 🎯 Functional Verification - ✅ All dashboard panels (model analysis, resource consumption, performance metrics) update correctly - ✅ Search functionality works with proper parameter validation - ✅ Chart data refreshes properly after search/filter operations - ✅ User interface remains identical to original implementation - ✅ All existing features preserved without regression ### 🔄 Data Flow ``` User Input → SearchModal → useDashboardData → API Call → useDashboardCharts → UI Update ``` ## Breaking Changes None. All existing functionality preserved. ## Migration Notes The refactored dashboard maintains 100% API compatibility and identical user experience while providing a cleaner, more maintainable codebase structure.
247 lines
8.3 KiB
JavaScript
247 lines
8.3 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/index.js';
|
|
import { StatusContext } from '../../context/Status/index.js';
|
|
|
|
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 => ({
|
|
...item,
|
|
time: getRelativeTime(item.publishDate)
|
|
}));
|
|
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;
|