📚 refactor(dashboard): modularize dashboard page into reusable hooks and components
## 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.
This commit is contained in:
247
web/src/components/dashboard/index.jsx
Normal file
247
web/src/components/dashboard/index.jsx
Normal file
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
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;
|
||||
Reference in New Issue
Block a user