Files
new-api-hunter/web/src/hooks/dashboard/useDashboardData.js
t0ng7u f1e34bbc97 📚 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.
2025-07-20 15:47:02 +08:00

313 lines
9.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
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 { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { API, isAdmin, showError, timestamp2string } from '../../helpers';
import { getDefaultTime, getInitialTimestamp } from '../../helpers/dashboard';
import { TIME_OPTIONS } from '../../constants/dashboard.constants';
import { useIsMobile } from '../common/useIsMobile';
export const useDashboardData = (userState, userDispatch, statusState) => {
const { t } = useTranslation();
const navigate = useNavigate();
const isMobile = useIsMobile();
const initialized = useRef(false);
// ========== 基础状态 ==========
const [loading, setLoading] = useState(false);
const [greetingVisible, setGreetingVisible] = useState(false);
const [searchModalVisible, setSearchModalVisible] = useState(false);
// ========== 输入状态 ==========
const [inputs, setInputs] = useState({
username: '',
token_name: '',
model_name: '',
start_timestamp: getInitialTimestamp(),
end_timestamp: timestamp2string(new Date().getTime() / 1000 + 3600),
channel: '',
data_export_default_time: '',
});
const [dataExportDefaultTime, setDataExportDefaultTime] = useState(getDefaultTime());
// ========== 数据状态 ==========
const [quotaData, setQuotaData] = useState([]);
const [consumeQuota, setConsumeQuota] = useState(0);
const [consumeTokens, setConsumeTokens] = useState(0);
const [times, setTimes] = useState(0);
const [pieData, setPieData] = useState([{ type: 'null', value: '0' }]);
const [lineData, setLineData] = useState([]);
const [modelColors, setModelColors] = useState({});
// ========== 图表状态 ==========
const [activeChartTab, setActiveChartTab] = useState('1');
// ========== 趋势数据 ==========
const [trendData, setTrendData] = useState({
balance: [],
usedQuota: [],
requestCount: [],
times: [],
consumeQuota: [],
tokens: [],
rpm: [],
tpm: []
});
// ========== Uptime 数据 ==========
const [uptimeData, setUptimeData] = useState([]);
const [uptimeLoading, setUptimeLoading] = useState(false);
const [activeUptimeTab, setActiveUptimeTab] = useState('');
// ========== 常量 ==========
const now = new Date();
const isAdminUser = isAdmin();
// ========== Panel enable flags ==========
const apiInfoEnabled = statusState?.status?.api_info_enabled ?? true;
const announcementsEnabled = statusState?.status?.announcements_enabled ?? true;
const faqEnabled = statusState?.status?.faq_enabled ?? true;
const uptimeEnabled = statusState?.status?.uptime_kuma_enabled ?? true;
const hasApiInfoPanel = apiInfoEnabled;
const hasInfoPanels = announcementsEnabled || faqEnabled || uptimeEnabled;
// ========== Memoized Values ==========
const timeOptions = useMemo(() => TIME_OPTIONS.map(option => ({
...option,
label: t(option.label)
})), [t]);
const performanceMetrics = useMemo(() => {
const { start_timestamp, end_timestamp } = inputs;
const timeDiff = (Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000;
const avgRPM = isNaN(times / timeDiff) ? '0' : (times / timeDiff).toFixed(3);
const avgTPM = isNaN(consumeTokens / timeDiff) ? '0' : (consumeTokens / timeDiff).toFixed(3);
return { avgRPM, avgTPM, timeDiff };
}, [times, consumeTokens, inputs.start_timestamp, inputs.end_timestamp]);
const getGreeting = useMemo(() => {
const hours = new Date().getHours();
let greeting = '';
if (hours >= 5 && hours < 12) {
greeting = t('早上好');
} else if (hours >= 12 && hours < 14) {
greeting = t('中午好');
} else if (hours >= 14 && hours < 18) {
greeting = t('下午好');
} else {
greeting = t('晚上好');
}
const username = userState?.user?.username || '';
return `👋${greeting}${username}`;
}, [t, userState?.user?.username]);
// ========== 回调函数 ==========
const handleInputChange = useCallback((value, name) => {
if (name === 'data_export_default_time') {
setDataExportDefaultTime(value);
localStorage.setItem('data_export_default_time', value);
return;
}
setInputs((inputs) => ({ ...inputs, [name]: value }));
}, []);
const showSearchModal = useCallback(() => {
setSearchModalVisible(true);
}, []);
const handleCloseModal = useCallback(() => {
setSearchModalVisible(false);
}, []);
// ========== API 调用函数 ==========
const loadQuotaData = useCallback(async () => {
setLoading(true);
const startTime = Date.now();
try {
let url = '';
const { start_timestamp, end_timestamp, username } = inputs;
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
if (isAdminUser) {
url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
} else {
url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
}
const res = await API.get(url);
const { success, message, data } = res.data;
if (success) {
setQuotaData(data);
if (data.length === 0) {
data.push({
count: 0,
model_name: '无数据',
quota: 0,
created_at: now.getTime() / 1000,
});
}
data.sort((a, b) => a.created_at - b.created_at);
return data;
} else {
showError(message);
return [];
}
} finally {
const elapsed = Date.now() - startTime;
const remainingTime = Math.max(0, 500 - elapsed);
setTimeout(() => {
setLoading(false);
}, remainingTime);
}
}, [inputs, dataExportDefaultTime, isAdminUser, now]);
const loadUptimeData = useCallback(async () => {
setUptimeLoading(true);
try {
const res = await API.get('/api/uptime/status');
const { success, message, data } = res.data;
if (success) {
setUptimeData(data || []);
if (data && data.length > 0 && !activeUptimeTab) {
setActiveUptimeTab(data[0].categoryName);
}
} else {
showError(message);
}
} catch (err) {
console.error(err);
} finally {
setUptimeLoading(false);
}
}, [activeUptimeTab]);
const getUserData = useCallback(async () => {
let res = await API.get(`/api/user/self`);
const { success, message, data } = res.data;
if (success) {
userDispatch({ type: 'login', payload: data });
} else {
showError(message);
}
}, [userDispatch]);
const refresh = useCallback(async () => {
const data = await loadQuotaData();
await loadUptimeData();
return data;
}, [loadQuotaData, loadUptimeData]);
const handleSearchConfirm = useCallback(async (updateChartDataCallback) => {
const data = await refresh();
if (data && data.length > 0 && updateChartDataCallback) {
updateChartDataCallback(data);
}
setSearchModalVisible(false);
}, [refresh]);
// ========== Effects ==========
useEffect(() => {
const timer = setTimeout(() => {
setGreetingVisible(true);
}, 100);
return () => clearTimeout(timer);
}, []);
useEffect(() => {
if (!initialized.current) {
getUserData();
initialized.current = true;
}
}, [getUserData]);
return {
// 基础状态
loading,
greetingVisible,
searchModalVisible,
// 输入状态
inputs,
dataExportDefaultTime,
// 数据状态
quotaData,
consumeQuota,
setConsumeQuota,
consumeTokens,
setConsumeTokens,
times,
setTimes,
pieData,
setPieData,
lineData,
setLineData,
modelColors,
setModelColors,
// 图表状态
activeChartTab,
setActiveChartTab,
// 趋势数据
trendData,
setTrendData,
// Uptime 数据
uptimeData,
uptimeLoading,
activeUptimeTab,
setActiveUptimeTab,
// 计算值
timeOptions,
performanceMetrics,
getGreeting,
isAdminUser,
hasApiInfoPanel,
hasInfoPanels,
apiInfoEnabled,
announcementsEnabled,
faqEnabled,
uptimeEnabled,
// 函数
handleInputChange,
showSearchModal,
handleCloseModal,
loadQuotaData,
loadUptimeData,
getUserData,
refresh,
handleSearchConfirm,
// 导航和翻译
navigate,
t,
isMobile
};
};