refactor: Standardize toast notifications by replacing antd message with ToastContext

main
mula.liu 2025-12-01 16:52:04 +08:00
parent 241a6cb562
commit e2a9052e57
13 changed files with 136 additions and 111 deletions

View File

@ -1,6 +1,5 @@
import { useState, useCallback, useEffect } from 'react'; import { useState, useCallback, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { message } from 'antd';
import { useSpaceData } from './hooks/useSpaceData'; import { useSpaceData } from './hooks/useSpaceData';
import { useHistoricalData } from './hooks/useHistoricalData'; import { useHistoricalData } from './hooks/useHistoricalData';
import { useTrajectory } from './hooks/useTrajectory'; import { useTrajectory } from './hooks/useTrajectory';
@ -16,6 +15,7 @@ import { AuthModal } from './components/AuthModal';
import { MessageBoard } from './components/MessageBoard'; import { MessageBoard } from './components/MessageBoard';
import { auth } from './utils/auth'; import { auth } from './utils/auth';
import type { CelestialBody } from './types'; import type { CelestialBody } from './types';
import { useToast } from './contexts/ToastContext';
// Timeline configuration - will be fetched from backend later // Timeline configuration - will be fetched from backend later
const TIMELINE_DAYS = 30; // Total days in timeline range const TIMELINE_DAYS = 30; // Total days in timeline range
@ -24,6 +24,7 @@ const PREFS_KEY = 'cosmo_preferences';
function App() { function App() {
const navigate = useNavigate(); const navigate = useNavigate();
const toast = useToast();
// Load preferences // Load preferences
const [isTimelineMode, setIsTimelineMode] = useState(false); // Usually not persisted const [isTimelineMode, setIsTimelineMode] = useState(false); // Usually not persisted
@ -102,7 +103,7 @@ function App() {
// Screenshot handler with auth check // Screenshot handler with auth check
const handleScreenshot = useCallback(() => { const handleScreenshot = useCallback(() => {
if (!user) { if (!user) {
message.warning('请先登录以拍摄宇宙快照'); toast.warning('请先登录以拍摄宇宙快照');
setShowAuthModal(true); setShowAuthModal(true);
return; return;
} }

View File

@ -1,8 +1,9 @@
import { X, Ruler, Activity, Radar } from 'lucide-react'; import { X, Ruler, Activity, Radar } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import { Modal, message, Spin } from 'antd';
import { request } from '../utils/request'; import { request } from '../utils/request';
import type { CelestialBody } from '../types'; import type { CelestialBody } from '../types';
import { TerminalModal } from './TerminalModal';
import { useToast } from '../contexts/ToastContext';
interface FocusInfoProps { interface FocusInfoProps {
body: CelestialBody | null; body: CelestialBody | null;
@ -10,6 +11,7 @@ interface FocusInfoProps {
} }
export function FocusInfo({ body, onClose }: FocusInfoProps) { export function FocusInfo({ body, onClose }: FocusInfoProps) {
const toast = useToast();
const [showTerminal, setShowTerminal] = useState(false); const [showTerminal, setShowTerminal] = useState(false);
const [terminalData, setTerminalData] = useState(''); const [terminalData, setTerminalData] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -31,7 +33,7 @@ export function FocusInfo({ body, onClose }: FocusInfoProps) {
setTerminalData(data.raw_data); setTerminalData(data.raw_data);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
message.error('连接 NASA Horizons 失败'); toast.error('连接 NASA Horizons 失败');
// If failed, maybe show error in terminal // If failed, maybe show error in terminal
setTerminalData("CONNECTION FAILED.\n\nError establishing link with JPL Horizons System.\nCheck connection frequencies."); setTerminalData("CONNECTION FAILED.\n\nError establishing link with JPL Horizons System.\nCheck connection frequencies.");
} finally { } finally {

View File

@ -1,9 +1,10 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { Input, Button, message } from 'antd'; import { Input, Button } from 'antd';
import { Send, MessageSquare } from 'lucide-react'; import { Send, MessageSquare } from 'lucide-react';
import { TerminalModal } from './TerminalModal'; import { TerminalModal } from './TerminalModal';
import { request } from '../utils/request'; import { request } from '../utils/request';
import { auth } from '../utils/auth'; import { auth } from '../utils/auth';
import { useToast } from '../contexts/ToastContext';
interface Message { interface Message {
id: string; id: string;
@ -19,6 +20,7 @@ interface MessageBoardProps {
} }
export function MessageBoard({ open, onClose }: MessageBoardProps) { export function MessageBoard({ open, onClose }: MessageBoardProps) {
const toast = useToast();
const [messages, setMessages] = useState<Message[]>([]); const [messages, setMessages] = useState<Message[]>([]);
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -64,12 +66,12 @@ export function MessageBoard({ open, onClose }: MessageBoardProps) {
const user = auth.getUser(); const user = auth.getUser();
if (!user) { if (!user) {
message.warning('请先登录'); toast.warning('请先登录');
return; return;
} }
if (content.length > 20) { if (content.length > 20) {
message.warning('消息不能超过20字'); toast.warning('消息不能超过20字');
return; return;
} }
@ -79,7 +81,7 @@ export function MessageBoard({ open, onClose }: MessageBoardProps) {
setInputValue(''); setInputValue('');
await fetchMessages(); await fetchMessages();
} catch (err) { } catch (err) {
message.error('发送失败'); toast.error('发送失败');
} finally { } finally {
setSending(false); setSending(false);
} }

View File

@ -12,11 +12,12 @@ interface Toast {
} }
interface ToastContextValue { interface ToastContextValue {
showToast: (message: string, type?: ToastType, duration?: number) => void; showToast: (message: string, type?: ToastType, duration?: number) => string;
success: (message: string, duration?: number) => void; success: (message: string, duration?: number) => string;
error: (message: string, duration?: number) => void; error: (message: string, duration?: number) => string;
warning: (message: string, duration?: number) => void; warning: (message: string, duration?: number) => string;
info: (message: string, duration?: number) => void; info: (message: string, duration?: number) => string;
removeToast: (id: string) => void;
} }
// Context // Context
@ -72,6 +73,8 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {
}, duration); }, duration);
timersRef.current.set(id, timer); timersRef.current.set(id, timer);
} }
return id;
}, [removeToast]); }, [removeToast]);
// Convenience methods // Convenience methods
@ -81,7 +84,7 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {
const info = useCallback((msg: string, d?: number) => showToast(msg, 'info', d), [showToast]); const info = useCallback((msg: string, d?: number) => showToast(msg, 'info', d), [showToast]);
return ( return (
<ToastContext.Provider value={{ showToast, success, error, warning, info }}> <ToastContext.Provider value={{ showToast, success, error, warning, info, removeToast }}>
{children} {children}
{/* Toast Container - Top Right */} {/* Toast Container - Top Right */}

View File

@ -1,18 +1,20 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import html2canvas from 'html2canvas'; import html2canvas from 'html2canvas';
import { message } from 'antd'; import { useToast } from '../contexts/ToastContext';
export function useScreenshot() { export function useScreenshot() {
const toast = useToast();
const takeScreenshot = useCallback(async (username: string = 'Explorer') => { const takeScreenshot = useCallback(async (username: string = 'Explorer') => {
// 1. Find the container that includes both the Canvas and the HTML overlays (labels) // 1. Find the container that includes both the Canvas and the HTML overlays (labels)
const element = document.getElementById('cosmo-scene-container'); const element = document.getElementById('cosmo-scene-container');
if (!element) { if (!element) {
console.error('Scene container not found'); console.error('Scene container not found');
message.error('无法找到截图区域'); toast.error('无法找到截图区域');
return; return;
} }
const hideMessage = message.loading('正在生成宇宙快照...', 0); const toastId = toast.info('正在生成宇宙快照...', 0);
try { try {
// 2. Use html2canvas to capture the visual composite // 2. Use html2canvas to capture the visual composite
@ -107,15 +109,15 @@ export function useScreenshot() {
link.href = dataUrl; link.href = dataUrl;
link.click(); link.click();
message.success('宇宙快照已保存'); toast.success('宇宙快照已保存');
} catch (err) { } catch (err) {
console.error('Screenshot failed:', err); console.error('Screenshot failed:', err);
message.error('截图失败,请稍后重试'); toast.error('截图失败,请稍后重试');
} finally { } finally {
hideMessage(); toast.removeToast(toastId);
} }
}, []); }, [toast]);
return { takeScreenshot }; return { takeScreenshot };
} }

View File

@ -3,30 +3,33 @@
*/ */
import { useState } from 'react'; import { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Form, Input, Button, Card, message } from 'antd'; import { Form, Input, Button, Card } from 'antd';
import { UserOutlined, LockOutlined } from '@ant-design/icons'; import { UserOutlined, LockOutlined } from '@ant-design/icons';
import { authAPI } from '../utils/request'; import { authAPI } from '../utils/request';
import { auth } from '../utils/auth'; import { auth } from '../utils/auth';
import { useToast } from '../contexts/ToastContext';
export function Login() { export function Login() {
const [loading, setLoading] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const toast = useToast();
const onFinish = async (values: { username: string; password: string }) => { const onFinish = async (values: { username: string; password: string }) => {
setLoading(true); setLoading(true);
try { try {
const { data } = await authAPI.login(values.username, values.password); const { data } = await authAPI.login(values.username, values.password);
// Save token and user info // Save token and user info
auth.setToken(data.access_token); auth.setToken(data.access_token);
auth.setUser(data.user); auth.setUser(data.user);
message.success('登录成功!'); toast.success('登录成功!');
// Redirect to admin dashboard // Navigate to admin dashboard
navigate('/admin'); navigate('/admin');
} catch (error: any) { } catch (error: any) {
message.error(error.response?.data?.detail || '登录失败,请检查用户名和密码'); console.error('Login failed:', error);
toast.error(error.response?.data?.detail || '登录失败,请检查用户名和密码');
} finally { } finally {
setLoading(false); setLoading(false);
} }

View File

@ -3,7 +3,7 @@
*/ */
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Outlet, useNavigate, useLocation } from 'react-router-dom'; import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import { Layout, Menu, Avatar, Dropdown, message } from 'antd'; import { Layout, Menu, Avatar, Dropdown } from 'antd';
import { import {
MenuFoldOutlined, MenuFoldOutlined,
MenuUnfoldOutlined, MenuUnfoldOutlined,
@ -21,6 +21,7 @@ import {
import type { MenuProps } from 'antd'; import type { MenuProps } from 'antd';
import { authAPI } from '../../utils/request'; import { authAPI } from '../../utils/request';
import { auth } from '../../utils/auth'; import { auth } from '../../utils/auth';
import { useToast } from '../../contexts/ToastContext';
const { Header, Sider, Content } = Layout; const { Header, Sider, Content } = Layout;
@ -43,6 +44,7 @@ export function AdminLayout() {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const user = auth.getUser(); const user = auth.getUser();
const toast = useToast();
// Load menus from backend // Load menus from backend
useEffect(() => { useEffect(() => {
@ -54,7 +56,7 @@ export function AdminLayout() {
const { data } = await authAPI.getMenus(); const { data } = await authAPI.getMenus();
setMenus(data); setMenus(data);
} catch (error) { } catch (error) {
message.error('加载菜单失败'); toast.error('加载菜单失败');
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -85,7 +87,7 @@ export function AdminLayout() {
try { try {
await authAPI.logout(); await authAPI.logout();
auth.logout(); auth.logout();
message.success('登出成功'); toast.success('登出成功');
navigate('/login'); navigate('/login');
} catch (error) { } catch (error) {
// Even if API fails, clear local auth // Even if API fails, clear local auth

View File

@ -1,13 +1,11 @@
/**
* Celestial Bodies Management Page
*/
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { message, Modal, Form, Input, Select, Switch, InputNumber, Tag, Badge, Descriptions, Button, Space, Alert, Upload, Popconfirm, Row, Col } from 'antd'; import { Modal, Form, Input, Select, Switch, InputNumber, Tag, Badge, Descriptions, Button, Space, Alert, Upload, Popconfirm, Row, Col } from 'antd';
import { CheckCircleOutlined, CloseCircleOutlined, SearchOutlined, UploadOutlined, DeleteOutlined } from '@ant-design/icons'; import { CheckCircleOutlined, CloseCircleOutlined, SearchOutlined, UploadOutlined, DeleteOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import type { UploadFile } from 'antd/es/upload/interface'; import type { UploadFile } from 'antd/es/upload/interface';
import type { ColumnsType } from 'antd/es/table';
import { DataTable } from '../../components/admin/DataTable'; import { DataTable } from '../../components/admin/DataTable';
import { request } from '../../utils/request'; import { request } from '../../utils/request';
import { useToast } from '../../contexts/ToastContext';
interface CelestialBody { interface CelestialBody {
id: string; id: string;
@ -38,6 +36,7 @@ export function CelestialBodies() {
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
const [refreshResources, setRefreshResources] = useState(0); const [refreshResources, setRefreshResources] = useState(0);
const toast = useToast();
useEffect(() => { useEffect(() => {
loadData(); loadData();
@ -50,7 +49,7 @@ export function CelestialBodies() {
setData(result.bodies || []); setData(result.bodies || []);
setFilteredData(result.bodies || []); setFilteredData(result.bodies || []);
} catch (error) { } catch (error) {
message.error('加载数据失败'); toast.error('加载数据失败');
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -81,7 +80,7 @@ export function CelestialBodies() {
// Search NASA Horizons by name // Search NASA Horizons by name
const handleNASASearch = async () => { const handleNASASearch = async () => {
if (!searchQuery.trim()) { if (!searchQuery.trim()) {
message.warning('请输入天体名称或ID'); toast.warning('请输入天体名称或ID');
return; return;
} }
@ -102,7 +101,7 @@ export function CelestialBodies() {
const isNumericId = /^-?\d+$/.test(result.data.id); const isNumericId = /^-?\d+$/.test(result.data.id);
if (isNumericId) { if (isNumericId) {
message.success(`找到天体: ${result.data.full_name}`); toast.success(`找到天体: ${result.data.full_name}`);
} else { } else {
// Warn user that ID might need manual correction // Warn user that ID might need manual correction
Modal.warning({ Modal.warning({
@ -122,10 +121,10 @@ export function CelestialBodies() {
}); });
} }
} else { } else {
message.error(result.error || '查询失败'); toast.error(result.error || '查询失败');
} }
} catch (error: any) { } catch (error: any) {
message.error(error.response?.data?.detail || '查询失败'); toast.error(error.response?.data?.detail || '查询失败');
} finally { } finally {
setSearching(false); setSearching(false);
} }
@ -142,10 +141,10 @@ export function CelestialBodies() {
const handleDelete = async (record: CelestialBody) => { const handleDelete = async (record: CelestialBody) => {
try { try {
await request.delete(`/celestial/${record.id}`); await request.delete(`/celestial/${record.id}`);
message.success('删除成功'); toast.success('删除成功');
loadData(); loadData();
} catch (error) { } catch (error) {
message.error('删除失败'); toast.error('删除失败');
} }
}; };
@ -153,7 +152,7 @@ export function CelestialBodies() {
const handleStatusChange = async (record: CelestialBody, checked: boolean) => { const handleStatusChange = async (record: CelestialBody, checked: boolean) => {
try { try {
await request.put(`/celestial/${record.id}`, { is_active: checked }); await request.put(`/celestial/${record.id}`, { is_active: checked });
message.success(`状态更新成功`); toast.success(`状态更新成功`);
// Update local state to avoid full reload // Update local state to avoid full reload
const newData = data.map(item => const newData = data.map(item =>
item.id === record.id ? { ...item, is_active: checked } : item item.id === record.id ? { ...item, is_active: checked } : item
@ -161,7 +160,7 @@ export function CelestialBodies() {
setData(newData); setData(newData);
setFilteredData(newData); // Should re-filter if needed, but simplistic here setFilteredData(newData); // Should re-filter if needed, but simplistic here
} catch (error) { } catch (error) {
message.error('状态更新失败'); toast.error('状态更新失败');
} }
}; };
@ -173,25 +172,25 @@ export function CelestialBodies() {
if (editingRecord) { if (editingRecord) {
// Update // Update
await request.put(`/celestial/${editingRecord.id}`, values); await request.put(`/celestial/${editingRecord.id}`, values);
message.success('更新成功'); toast.success('更新成功');
} else { } else {
// Create // Create
await request.post('/celestial/', values); await request.post('/celestial/', values);
message.success('创建成功'); toast.success('创建成功');
} }
setIsModalOpen(false); setIsModalOpen(false);
loadData(); loadData();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
// message.error('操作失败'); // request interceptor might already handle this // toast.error('操作失败'); // request interceptor might already handle this
} }
}; };
// Handle resource upload // Handle resource upload
const handleResourceUpload = async (file: File, resourceType: string) => { const handleResourceUpload = async (file: File, resourceType: string) => {
if (!editingRecord) { if (!editingRecord) {
message.error('请先选择要编辑的天体'); toast.error('请先选择要编辑的天体');
return false; return false;
} }
@ -210,11 +209,11 @@ export function CelestialBodies() {
} }
); );
message.success(`${response.data.message} (上传到 ${response.data.upload_directory} 目录)`); toast.success(`${response.data.message} (上传到 ${response.data.upload_directory} 目录)`);
setRefreshResources(prev => prev + 1); // Trigger reload setRefreshResources(prev => prev + 1); // Trigger reload
return false; // Prevent default upload behavior return false; // Prevent default upload behavior
} catch (error: any) { } catch (error: any) {
message.error(error.response?.data?.detail || '上传失败'); toast.error(error.response?.data?.detail || '上传失败');
return false; return false;
} finally { } finally {
setUploading(false); setUploading(false);
@ -225,10 +224,10 @@ export function CelestialBodies() {
const handleResourceDelete = async (resourceId: number) => { const handleResourceDelete = async (resourceId: number) => {
try { try {
await request.delete(`/celestial/resources/${resourceId}`); await request.delete(`/celestial/resources/${resourceId}`);
message.success('删除成功'); toast.success('删除成功');
setRefreshResources(prev => prev + 1); // Trigger reload setRefreshResources(prev => prev + 1); // Trigger reload
} catch (error: any) { } catch (error: any) {
message.error(error.response?.data?.detail || '删除失败'); toast.error(error.response?.data?.detail || '删除失败');
} }
}; };
@ -433,6 +432,7 @@ export function CelestialBodies() {
onDelete={handleResourceDelete} onDelete={handleResourceDelete}
uploading={uploading} uploading={uploading}
refreshTrigger={refreshResources} refreshTrigger={refreshResources}
toast={toast}
/> />
)} )}
</Form> </Form>
@ -451,6 +451,7 @@ function ResourceManager({
onDelete, onDelete,
uploading, uploading,
refreshTrigger, refreshTrigger,
toast,
}: { }: {
bodyId: string; bodyId: string;
bodyType: string; bodyType: string;
@ -460,6 +461,7 @@ function ResourceManager({
onDelete: (resourceId: number) => Promise<void>; onDelete: (resourceId: number) => Promise<void>;
uploading: boolean; uploading: boolean;
refreshTrigger: number; refreshTrigger: number;
toast: any;
}) { }) {
const [currentResources, setCurrentResources] = useState(resources); const [currentResources, setCurrentResources] = useState(resources);
@ -477,7 +479,7 @@ function ResourceManager({
setCurrentResources(grouped); setCurrentResources(grouped);
}) })
.catch(() => { .catch(() => {
message.error('加载资源列表失败'); toast.error('加载资源列表失败');
}); });
}, [refreshTrigger, bodyId]); }, [refreshTrigger, bodyId]);
@ -547,9 +549,9 @@ function ResourceManager({
request.put(`/celestial/resources/${res.id}`, { request.put(`/celestial/resources/${res.id}`, {
extra_data: { ...res.extra_data, scale: newScale } extra_data: { ...res.extra_data, scale: newScale }
}).then(() => { }).then(() => {
message.success('缩放参数已更新'); toast.success('缩放参数已更新');
}).catch(() => { }).catch(() => {
message.error('更新失败'); toast.error('更新失败');
}); });
}} }}
/> />

View File

@ -1,14 +1,16 @@
/** /**
* Dashboard Page * Dashboard Page
*/ */
import { Card, Row, Col, Statistic, message } from 'antd'; import { Card, Row, Col, Statistic } from 'antd';
import { GlobalOutlined, RocketOutlined, UserOutlined } from '@ant-design/icons'; import { GlobalOutlined, RocketOutlined, UserOutlined } from '@ant-design/icons';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { request } from '../../utils/request'; import { request } from '../../utils/request';
import { useToast } from '../../contexts/ToastContext';
export function Dashboard() { export function Dashboard() {
const [totalUsers, setTotalUsers] = useState<number | null>(null); const [totalUsers, setTotalUsers] = useState<number | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const toast = useToast();
useEffect(() => { useEffect(() => {
const fetchUserCount = async () => { const fetchUserCount = async () => {
@ -19,7 +21,7 @@ export function Dashboard() {
setTotalUsers(response.data.total_users); setTotalUsers(response.data.total_users);
} catch (error) { } catch (error) {
console.error('Failed to fetch user count:', error); console.error('Failed to fetch user count:', error);
message.error('无法获取用户总数'); toast.error('无法获取用户总数');
setTotalUsers(0); // Set to 0 or handle error display setTotalUsers(0); // Set to 0 or handle error display
} finally { } finally {
setLoading(false); setLoading(false);

View File

@ -10,7 +10,6 @@ import {
Checkbox, Checkbox,
DatePicker, DatePicker,
Button, Button,
message,
Badge, Badge,
Spin, Spin,
Typography, Typography,
@ -31,6 +30,7 @@ import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import isBetween from 'dayjs/plugin/isBetween'; import isBetween from 'dayjs/plugin/isBetween';
import { request } from '../../utils/request'; import { request } from '../../utils/request';
import { useToast } from '../../contexts/ToastContext';
// Extend dayjs with isBetween plugin // Extend dayjs with isBetween plugin
dayjs.extend(isBetween); dayjs.extend(isBetween);
@ -63,6 +63,7 @@ export function NASADownload() {
const [loadingDates, setLoadingDates] = useState(false); const [loadingDates, setLoadingDates] = useState(false);
const [downloading, setDownloading] = useState(false); const [downloading, setDownloading] = useState(false);
const [downloadProgress, setDownloadProgress] = useState({ current: 0, total: 0 }); const [downloadProgress, setDownloadProgress] = useState({ current: 0, total: 0 });
const toast = useToast();
// Type name mapping // Type name mapping
const typeNames: Record<string, string> = { const typeNames: Record<string, string> = {
@ -91,7 +92,7 @@ export function NASADownload() {
const { data } = await request.get('/celestial/positions/download/bodies'); const { data } = await request.get('/celestial/positions/download/bodies');
setBodies(data.bodies || {}); setBodies(data.bodies || {});
} catch (error) { } catch (error) {
message.error('加载天体列表失败'); toast.error('加载天体列表失败');
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -123,7 +124,7 @@ export function NASADownload() {
setAvailableDates(allDates); setAvailableDates(allDates);
} catch (error) { } catch (error) {
message.error('加载数据状态失败'); toast.error('加载数据状态失败');
} finally { } finally {
setLoadingDates(false); setLoadingDates(false);
} }
@ -154,7 +155,7 @@ export function NASADownload() {
const handleDownload = async (selectedDate?: Dayjs) => { const handleDownload = async (selectedDate?: Dayjs) => {
if (selectedBodies.length === 0) { if (selectedBodies.length === 0) {
message.warning('请先选择至少一个天体'); toast.warning('请先选择至少一个天体');
return; return;
} }
@ -189,10 +190,10 @@ export function NASADownload() {
setDownloadProgress({ current: datesToDownload.length, total: datesToDownload.length }); setDownloadProgress({ current: datesToDownload.length, total: datesToDownload.length });
if (data.total_success > 0) { if (data.total_success > 0) {
message.success(`成功下载 ${data.total_success} 条数据${data.total_failed > 0 ? `${data.total_failed} 条失败` : ''}`); toast.success(`成功下载 ${data.total_success} 条数据${data.total_failed > 0 ? `${data.total_failed} 条失败` : ''}`);
loadAvailableDates(); loadAvailableDates();
} else { } else {
message.error('下载失败'); toast.error('下载失败');
} }
} else { } else {
// Async download for range // Async download for range
@ -200,10 +201,10 @@ export function NASADownload() {
body_ids: selectedBodies, body_ids: selectedBodies,
dates: datesToDownload dates: datesToDownload
}); });
message.success('后台下载任务已启动,请前往“系统任务”查看进度'); toast.success('后台下载任务已启动,请前往“系统任务”查看进度');
} }
} catch (error) { } catch (error) {
message.error('请求失败'); toast.error('请求失败');
} finally { } finally {
setDownloading(false); setDownloading(false);
setDownloadProgress({ current: 0, total: 0 }); setDownloadProgress({ current: 0, total: 0 });
@ -240,17 +241,17 @@ export function NASADownload() {
const inRange = date.isBetween(dateRange[0], dateRange[1], 'day', '[]'); const inRange = date.isBetween(dateRange[0], dateRange[1], 'day', '[]');
if (!inRange) { if (!inRange) {
message.warning('请选择在日期范围内的日期'); toast.warning('请选择在日期范围内的日期');
return; return;
} }
if (hasData) { if (hasData) {
message.info('该日期已有数据'); toast.info('该日期已有数据');
return; return;
} }
if (selectedBodies.length === 0) { if (selectedBodies.length === 0) {
message.warning('请先选择天体'); toast.warning('请先选择天体');
return; return;
} }

View File

@ -2,10 +2,11 @@
* Static Data Management Page * Static Data Management Page
*/ */
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { message, Modal, Form, Input, Select } from 'antd'; import { Modal, Form, Input, Select } from 'antd';
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
import { DataTable } from '../../components/admin/DataTable'; import { DataTable } from '../../components/admin/DataTable';
import { request } from '../../utils/request'; import { request } from '../../utils/request';
import { useToast } from '../../contexts/ToastContext';
interface StaticDataItem { interface StaticDataItem {
id: number; id: number;
@ -22,6 +23,7 @@ export function StaticData() {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [editingRecord, setEditingRecord] = useState<StaticDataItem | null>(null); const [editingRecord, setEditingRecord] = useState<StaticDataItem | null>(null);
const [form] = Form.useForm(); const [form] = Form.useForm();
const toast = useToast();
useEffect(() => { useEffect(() => {
loadData(); loadData();
@ -34,7 +36,7 @@ export function StaticData() {
setData(result.items || []); setData(result.items || []);
setFilteredData(result.items || []); setFilteredData(result.items || []);
} catch (error) { } catch (error) {
message.error('加载数据失败'); toast.error('加载数据失败');
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -71,10 +73,10 @@ export function StaticData() {
const handleDelete = async (record: StaticDataItem) => { const handleDelete = async (record: StaticDataItem) => {
try { try {
await request.delete(`/celestial/static/${record.id}`); await request.delete(`/celestial/static/${record.id}`);
message.success('删除成功'); toast.success('删除成功');
loadData(); loadData();
} catch (error) { } catch (error) {
message.error('删除失败'); toast.error('删除失败');
} }
}; };
@ -86,16 +88,16 @@ export function StaticData() {
try { try {
values.data = JSON.parse(values.data); values.data = JSON.parse(values.data);
} catch (e) { } catch (e) {
message.error('JSON格式错误'); toast.error('JSON格式错误');
return; return;
} }
if (editingRecord) { if (editingRecord) {
await request.put(`/celestial/static/${editingRecord.id}`, values); await request.put(`/celestial/static/${editingRecord.id}`, values);
message.success('更新成功'); toast.success('更新成功');
} else { } else {
await request.post('/celestial/static', values); await request.post('/celestial/static', values);
message.success('创建成功'); toast.success('创建成功');
} }
setIsModalOpen(false); setIsModalOpen(false);

View File

@ -2,11 +2,12 @@
* System Settings Management Page * System Settings Management Page
*/ */
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { message, Modal, Form, Input, InputNumber, Switch, Select, Button, Card, Descriptions, Badge, Space, Popconfirm, Alert, Divider } from 'antd'; import { Modal, Form, Input, InputNumber, Switch, Select, Button, Card, Descriptions, Badge, Space, Popconfirm, Alert, Divider } from 'antd';
import { ReloadOutlined, ClearOutlined, WarningOutlined } from '@ant-design/icons'; import { ReloadOutlined, ClearOutlined, WarningOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
import { DataTable } from '../../components/admin/DataTable'; import { DataTable } from '../../components/admin/DataTable';
import { request } from '../../utils/request'; import { request } from '../../utils/request';
import { useToast } from '../../contexts/ToastContext';
interface SystemSetting { interface SystemSetting {
id: number; id: number;
@ -38,6 +39,7 @@ export function SystemSettings() {
const [editingRecord, setEditingRecord] = useState<SystemSetting | null>(null); const [editingRecord, setEditingRecord] = useState<SystemSetting | null>(null);
const [form] = Form.useForm(); const [form] = Form.useForm();
const [clearingCache, setClearingCache] = useState(false); const [clearingCache, setClearingCache] = useState(false);
const toast = useToast();
useEffect(() => { useEffect(() => {
loadData(); loadData();
@ -50,7 +52,7 @@ export function SystemSettings() {
setData(result.settings || []); setData(result.settings || []);
setFilteredData(result.settings || []); setFilteredData(result.settings || []);
} catch (error) { } catch (error) {
message.error('加载数据失败'); toast.error('加载数据失败');
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -95,10 +97,10 @@ export function SystemSettings() {
const handleDelete = async (record: SystemSetting) => { const handleDelete = async (record: SystemSetting) => {
try { try {
await request.delete(`/system/settings/${record.key}`); await request.delete(`/system/settings/${record.key}`);
message.success('删除成功'); toast.success('删除成功');
loadData(); loadData();
} catch (error) { } catch (error) {
message.error('删除失败'); toast.error('删除失败');
} }
}; };
@ -110,11 +112,11 @@ export function SystemSettings() {
if (editingRecord) { if (editingRecord) {
// Update // Update
await request.put(`/system/settings/${editingRecord.key}`, values); await request.put(`/system/settings/${editingRecord.key}`, values);
message.success('更新成功'); toast.success('更新成功');
} else { } else {
// Create // Create
await request.post('/system/settings', values); await request.post('/system/settings', values);
message.success('创建成功'); toast.success('创建成功');
} }
setIsModalOpen(false); setIsModalOpen(false);
@ -129,7 +131,7 @@ export function SystemSettings() {
setClearingCache(true); setClearingCache(true);
try { try {
const { data } = await request.post('/system/cache/clear'); const { data } = await request.post('/system/cache/clear');
message.success( toast.success(
<> <>
<div>{data.message}</div> <div>{data.message}</div>
<div style={{ fontSize: 12, color: '#888', marginTop: 4 }}> <div style={{ fontSize: 12, color: '#888', marginTop: 4 }}>
@ -140,7 +142,7 @@ export function SystemSettings() {
); );
loadData(); loadData();
} catch (error) { } catch (error) {
message.error('清除缓存失败'); toast.error('清除缓存失败');
} finally { } finally {
setClearingCache(false); setClearingCache(false);
} }

View File

@ -2,31 +2,28 @@
* User Management Page * User Management Page
*/ */
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { message, Modal, Button, Popconfirm } from 'antd'; import { Modal, Button, Popconfirm } from 'antd';
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
import { DataTable } from '../../components/admin/DataTable';
import { request } from '../../utils/request'; import { request } from '../../utils/request';
import { ReloadOutlined } from '@ant-design/icons'; import { DataTable } from '../../components/admin/DataTable';
import { useToast } from '../../contexts/ToastContext';
interface UserItem { interface UserItem {
id: number; id: number;
username: string; username: string;
full_name: string; email: string | null;
email: string; full_name: string | null;
is_active: boolean; is_active: boolean;
roles: string[]; roles: string[];
last_login_at: string; last_login_at: string | null;
created_at: string; created_at: string;
} }
export function Users() { export function Users() {
const [loading, setLoading] = useState(false);
const [data, setData] = useState<UserItem[]>([]); const [data, setData] = useState<UserItem[]>([]);
const [filteredData, setFilteredData] = useState<UserItem[]>([]); const [filteredData, setFilteredData] = useState<UserItem[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => { const toast = useToast();
loadData();
}, []);
const loadData = async () => { const loadData = async () => {
setLoading(true); setLoading(true);
@ -35,19 +32,23 @@ export function Users() {
setData(result.users || []); setData(result.users || []);
setFilteredData(result.users || []); setFilteredData(result.users || []);
} catch (error) { } catch (error) {
message.error('加载用户数据失败'); console.error(error);
toast.error('加载用户数据失败');
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
useEffect(() => {
loadData();
}, []);
const handleSearch = (keyword: string) => { const handleSearch = (keyword: string) => {
const lowerKeyword = keyword.toLowerCase(); const lowerKeyword = keyword.toLowerCase();
const filtered = data.filter( const filtered = data.filter(item =>
(item) => item.username.toLowerCase().includes(lowerKeyword) ||
item.username.toLowerCase().includes(lowerKeyword) || (item.email && item.email.toLowerCase().includes(lowerKeyword)) ||
item.full_name?.toLowerCase().includes(lowerKeyword) || (item.full_name && item.full_name.toLowerCase().includes(lowerKeyword))
item.email?.toLowerCase().includes(lowerKeyword)
); );
setFilteredData(filtered); setFilteredData(filtered);
}; };
@ -55,24 +56,24 @@ export function Users() {
const handleStatusChange = async (record: UserItem, checked: boolean) => { const handleStatusChange = async (record: UserItem, checked: boolean) => {
try { try {
await request.put(`/users/${record.id}/status`, { is_active: checked }); await request.put(`/users/${record.id}/status`, { is_active: checked });
message.success(`用户 ${record.username} 状态更新成功`); toast.success(`用户 ${record.username} 状态更新成功`);
// Update local state
const newData = data.map(item => const newData = data.map(item => item.id === record.id ? { ...item, is_active: checked } : item);
item.id === record.id ? { ...item, is_active: checked } : item
);
setData(newData); setData(newData);
setFilteredData(newData); setFilteredData(newData); // Also update filtered view if needed, simplified here
loadData(); // Reload to be sure
} catch (error) { } catch (error) {
message.error('状态更新失败'); console.error(error);
toast.error('状态更新失败');
} }
}; };
const handleResetPassword = async (record: UserItem) => { const handleResetPassword = async (record: UserItem) => {
try { try {
await request.post(`/users/${record.id}/reset-password`); await request.post(`/users/${record.id}/reset-password`);
message.success(`用户 ${record.username} 密码已重置`); toast.success(`用户 ${record.username} 密码已重置`);
} catch (error) { } catch (error) {
message.error('密码重置失败'); toast.error('密码重置失败');
} }
}; };