diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7ffcc70..a021043 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,5 @@ import { useState, useCallback, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import { message } from 'antd'; import { useSpaceData } from './hooks/useSpaceData'; import { useHistoricalData } from './hooks/useHistoricalData'; import { useTrajectory } from './hooks/useTrajectory'; @@ -16,6 +15,7 @@ import { AuthModal } from './components/AuthModal'; import { MessageBoard } from './components/MessageBoard'; import { auth } from './utils/auth'; import type { CelestialBody } from './types'; +import { useToast } from './contexts/ToastContext'; // Timeline configuration - will be fetched from backend later const TIMELINE_DAYS = 30; // Total days in timeline range @@ -24,6 +24,7 @@ const PREFS_KEY = 'cosmo_preferences'; function App() { const navigate = useNavigate(); + const toast = useToast(); // Load preferences const [isTimelineMode, setIsTimelineMode] = useState(false); // Usually not persisted @@ -102,7 +103,7 @@ function App() { // Screenshot handler with auth check const handleScreenshot = useCallback(() => { if (!user) { - message.warning('请先登录以拍摄宇宙快照'); + toast.warning('请先登录以拍摄宇宙快照'); setShowAuthModal(true); return; } diff --git a/frontend/src/components/FocusInfo.tsx b/frontend/src/components/FocusInfo.tsx index 085af1e..91947fb 100644 --- a/frontend/src/components/FocusInfo.tsx +++ b/frontend/src/components/FocusInfo.tsx @@ -1,8 +1,9 @@ import { X, Ruler, Activity, Radar } from 'lucide-react'; import { useState } from 'react'; -import { Modal, message, Spin } from 'antd'; import { request } from '../utils/request'; import type { CelestialBody } from '../types'; +import { TerminalModal } from './TerminalModal'; +import { useToast } from '../contexts/ToastContext'; interface FocusInfoProps { body: CelestialBody | null; @@ -10,6 +11,7 @@ interface FocusInfoProps { } export function FocusInfo({ body, onClose }: FocusInfoProps) { + const toast = useToast(); const [showTerminal, setShowTerminal] = useState(false); const [terminalData, setTerminalData] = useState(''); const [loading, setLoading] = useState(false); @@ -31,7 +33,7 @@ export function FocusInfo({ body, onClose }: FocusInfoProps) { setTerminalData(data.raw_data); } catch (err) { console.error(err); - message.error('连接 NASA Horizons 失败'); + toast.error('连接 NASA Horizons 失败'); // If failed, maybe show error in terminal setTerminalData("CONNECTION FAILED.\n\nError establishing link with JPL Horizons System.\nCheck connection frequencies."); } finally { diff --git a/frontend/src/components/MessageBoard.tsx b/frontend/src/components/MessageBoard.tsx index c03f51a..4d9ac3a 100644 --- a/frontend/src/components/MessageBoard.tsx +++ b/frontend/src/components/MessageBoard.tsx @@ -1,9 +1,10 @@ import { useState, useEffect, useRef } from 'react'; -import { Input, Button, message } from 'antd'; +import { Input, Button } from 'antd'; import { Send, MessageSquare } from 'lucide-react'; import { TerminalModal } from './TerminalModal'; import { request } from '../utils/request'; import { auth } from '../utils/auth'; +import { useToast } from '../contexts/ToastContext'; interface Message { id: string; @@ -19,6 +20,7 @@ interface MessageBoardProps { } export function MessageBoard({ open, onClose }: MessageBoardProps) { + const toast = useToast(); const [messages, setMessages] = useState([]); const [inputValue, setInputValue] = useState(''); const [loading, setLoading] = useState(false); @@ -64,12 +66,12 @@ export function MessageBoard({ open, onClose }: MessageBoardProps) { const user = auth.getUser(); if (!user) { - message.warning('请先登录'); + toast.warning('请先登录'); return; } if (content.length > 20) { - message.warning('消息不能超过20字'); + toast.warning('消息不能超过20字'); return; } @@ -79,7 +81,7 @@ export function MessageBoard({ open, onClose }: MessageBoardProps) { setInputValue(''); await fetchMessages(); } catch (err) { - message.error('发送失败'); + toast.error('发送失败'); } finally { setSending(false); } diff --git a/frontend/src/contexts/ToastContext.tsx b/frontend/src/contexts/ToastContext.tsx index 229b9e9..d1d9bcc 100644 --- a/frontend/src/contexts/ToastContext.tsx +++ b/frontend/src/contexts/ToastContext.tsx @@ -12,11 +12,12 @@ interface Toast { } interface ToastContextValue { - showToast: (message: string, type?: ToastType, duration?: number) => void; - success: (message: string, duration?: number) => void; - error: (message: string, duration?: number) => void; - warning: (message: string, duration?: number) => void; - info: (message: string, duration?: number) => void; + showToast: (message: string, type?: ToastType, duration?: number) => string; + success: (message: string, duration?: number) => string; + error: (message: string, duration?: number) => string; + warning: (message: string, duration?: number) => string; + info: (message: string, duration?: number) => string; + removeToast: (id: string) => void; } // Context @@ -72,6 +73,8 @@ export function ToastProvider({ children }: { children: React.ReactNode }) { }, duration); timersRef.current.set(id, timer); } + + return id; }, [removeToast]); // 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]); return ( - + {children} {/* Toast Container - Top Right */} diff --git a/frontend/src/hooks/useScreenshot.ts b/frontend/src/hooks/useScreenshot.ts index fbce3d3..f23d953 100644 --- a/frontend/src/hooks/useScreenshot.ts +++ b/frontend/src/hooks/useScreenshot.ts @@ -1,18 +1,20 @@ import { useCallback } from 'react'; import html2canvas from 'html2canvas'; -import { message } from 'antd'; +import { useToast } from '../contexts/ToastContext'; export function useScreenshot() { + const toast = useToast(); + const takeScreenshot = useCallback(async (username: string = 'Explorer') => { // 1. Find the container that includes both the Canvas and the HTML overlays (labels) const element = document.getElementById('cosmo-scene-container'); if (!element) { console.error('Scene container not found'); - message.error('无法找到截图区域'); + toast.error('无法找到截图区域'); return; } - const hideMessage = message.loading('正在生成宇宙快照...', 0); + const toastId = toast.info('正在生成宇宙快照...', 0); try { // 2. Use html2canvas to capture the visual composite @@ -107,15 +109,15 @@ export function useScreenshot() { link.href = dataUrl; link.click(); - message.success('宇宙快照已保存'); + toast.success('宇宙快照已保存'); } catch (err) { console.error('Screenshot failed:', err); - message.error('截图失败,请稍后重试'); + toast.error('截图失败,请稍后重试'); } finally { - hideMessage(); + toast.removeToast(toastId); } - }, []); + }, [toast]); return { takeScreenshot }; } \ No newline at end of file diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 2bf7204..66d8861 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -3,30 +3,33 @@ */ import { useState } from 'react'; 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 { authAPI } from '../utils/request'; import { auth } from '../utils/auth'; +import { useToast } from '../contexts/ToastContext'; export function Login() { - const [loading, setLoading] = useState(false); const navigate = useNavigate(); + const [loading, setLoading] = useState(false); + const toast = useToast(); const onFinish = async (values: { username: string; password: string }) => { setLoading(true); try { const { data } = await authAPI.login(values.username, values.password); - + // Save token and user info auth.setToken(data.access_token); auth.setUser(data.user); - - message.success('登录成功!'); - - // Redirect to admin dashboard + + toast.success('登录成功!'); + + // Navigate to admin dashboard navigate('/admin'); } catch (error: any) { - message.error(error.response?.data?.detail || '登录失败,请检查用户名和密码'); + console.error('Login failed:', error); + toast.error(error.response?.data?.detail || '登录失败,请检查用户名和密码'); } finally { setLoading(false); } diff --git a/frontend/src/pages/admin/AdminLayout.tsx b/frontend/src/pages/admin/AdminLayout.tsx index 58c2089..b5d235d 100644 --- a/frontend/src/pages/admin/AdminLayout.tsx +++ b/frontend/src/pages/admin/AdminLayout.tsx @@ -3,7 +3,7 @@ */ import { useState, useEffect } from 'react'; import { Outlet, useNavigate, useLocation } from 'react-router-dom'; -import { Layout, Menu, Avatar, Dropdown, message } from 'antd'; +import { Layout, Menu, Avatar, Dropdown } from 'antd'; import { MenuFoldOutlined, MenuUnfoldOutlined, @@ -21,6 +21,7 @@ import { import type { MenuProps } from 'antd'; import { authAPI } from '../../utils/request'; import { auth } from '../../utils/auth'; +import { useToast } from '../../contexts/ToastContext'; const { Header, Sider, Content } = Layout; @@ -43,6 +44,7 @@ export function AdminLayout() { const navigate = useNavigate(); const location = useLocation(); const user = auth.getUser(); + const toast = useToast(); // Load menus from backend useEffect(() => { @@ -54,7 +56,7 @@ export function AdminLayout() { const { data } = await authAPI.getMenus(); setMenus(data); } catch (error) { - message.error('加载菜单失败'); + toast.error('加载菜单失败'); } finally { setLoading(false); } @@ -85,7 +87,7 @@ export function AdminLayout() { try { await authAPI.logout(); auth.logout(); - message.success('登出成功'); + toast.success('登出成功'); navigate('/login'); } catch (error) { // Even if API fails, clear local auth diff --git a/frontend/src/pages/admin/CelestialBodies.tsx b/frontend/src/pages/admin/CelestialBodies.tsx index 4fce50d..441d4a8 100644 --- a/frontend/src/pages/admin/CelestialBodies.tsx +++ b/frontend/src/pages/admin/CelestialBodies.tsx @@ -1,13 +1,11 @@ -/** - * Celestial Bodies Management Page - */ 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 type { ColumnsType } from 'antd/es/table'; import type { UploadFile } from 'antd/es/upload/interface'; +import type { ColumnsType } from 'antd/es/table'; import { DataTable } from '../../components/admin/DataTable'; import { request } from '../../utils/request'; +import { useToast } from '../../contexts/ToastContext'; interface CelestialBody { id: string; @@ -38,6 +36,7 @@ export function CelestialBodies() { const [searchQuery, setSearchQuery] = useState(''); const [uploading, setUploading] = useState(false); const [refreshResources, setRefreshResources] = useState(0); + const toast = useToast(); useEffect(() => { loadData(); @@ -50,7 +49,7 @@ export function CelestialBodies() { setData(result.bodies || []); setFilteredData(result.bodies || []); } catch (error) { - message.error('加载数据失败'); + toast.error('加载数据失败'); } finally { setLoading(false); } @@ -81,7 +80,7 @@ export function CelestialBodies() { // Search NASA Horizons by name const handleNASASearch = async () => { if (!searchQuery.trim()) { - message.warning('请输入天体名称或ID'); + toast.warning('请输入天体名称或ID'); return; } @@ -102,7 +101,7 @@ export function CelestialBodies() { const isNumericId = /^-?\d+$/.test(result.data.id); if (isNumericId) { - message.success(`找到天体: ${result.data.full_name}`); + toast.success(`找到天体: ${result.data.full_name}`); } else { // Warn user that ID might need manual correction Modal.warning({ @@ -122,10 +121,10 @@ export function CelestialBodies() { }); } } else { - message.error(result.error || '查询失败'); + toast.error(result.error || '查询失败'); } } catch (error: any) { - message.error(error.response?.data?.detail || '查询失败'); + toast.error(error.response?.data?.detail || '查询失败'); } finally { setSearching(false); } @@ -142,10 +141,10 @@ export function CelestialBodies() { const handleDelete = async (record: CelestialBody) => { try { await request.delete(`/celestial/${record.id}`); - message.success('删除成功'); + toast.success('删除成功'); loadData(); } catch (error) { - message.error('删除失败'); + toast.error('删除失败'); } }; @@ -153,7 +152,7 @@ export function CelestialBodies() { const handleStatusChange = async (record: CelestialBody, checked: boolean) => { try { await request.put(`/celestial/${record.id}`, { is_active: checked }); - message.success(`状态更新成功`); + toast.success(`状态更新成功`); // Update local state to avoid full reload const newData = data.map(item => item.id === record.id ? { ...item, is_active: checked } : item @@ -161,7 +160,7 @@ export function CelestialBodies() { setData(newData); setFilteredData(newData); // Should re-filter if needed, but simplistic here } catch (error) { - message.error('状态更新失败'); + toast.error('状态更新失败'); } }; @@ -173,25 +172,25 @@ export function CelestialBodies() { if (editingRecord) { // Update await request.put(`/celestial/${editingRecord.id}`, values); - message.success('更新成功'); + toast.success('更新成功'); } else { // Create await request.post('/celestial/', values); - message.success('创建成功'); + toast.success('创建成功'); } setIsModalOpen(false); loadData(); } catch (error) { console.error(error); - // message.error('操作失败'); // request interceptor might already handle this + // toast.error('操作失败'); // request interceptor might already handle this } }; // Handle resource upload const handleResourceUpload = async (file: File, resourceType: string) => { if (!editingRecord) { - message.error('请先选择要编辑的天体'); + toast.error('请先选择要编辑的天体'); 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 return false; // Prevent default upload behavior } catch (error: any) { - message.error(error.response?.data?.detail || '上传失败'); + toast.error(error.response?.data?.detail || '上传失败'); return false; } finally { setUploading(false); @@ -225,10 +224,10 @@ export function CelestialBodies() { const handleResourceDelete = async (resourceId: number) => { try { await request.delete(`/celestial/resources/${resourceId}`); - message.success('删除成功'); + toast.success('删除成功'); setRefreshResources(prev => prev + 1); // Trigger reload } catch (error: any) { - message.error(error.response?.data?.detail || '删除失败'); + toast.error(error.response?.data?.detail || '删除失败'); } }; @@ -433,6 +432,7 @@ export function CelestialBodies() { onDelete={handleResourceDelete} uploading={uploading} refreshTrigger={refreshResources} + toast={toast} /> )} @@ -451,6 +451,7 @@ function ResourceManager({ onDelete, uploading, refreshTrigger, + toast, }: { bodyId: string; bodyType: string; @@ -460,6 +461,7 @@ function ResourceManager({ onDelete: (resourceId: number) => Promise; uploading: boolean; refreshTrigger: number; + toast: any; }) { const [currentResources, setCurrentResources] = useState(resources); @@ -477,7 +479,7 @@ function ResourceManager({ setCurrentResources(grouped); }) .catch(() => { - message.error('加载资源列表失败'); + toast.error('加载资源列表失败'); }); }, [refreshTrigger, bodyId]); @@ -547,9 +549,9 @@ function ResourceManager({ request.put(`/celestial/resources/${res.id}`, { extra_data: { ...res.extra_data, scale: newScale } }).then(() => { - message.success('缩放参数已更新'); + toast.success('缩放参数已更新'); }).catch(() => { - message.error('更新失败'); + toast.error('更新失败'); }); }} /> diff --git a/frontend/src/pages/admin/Dashboard.tsx b/frontend/src/pages/admin/Dashboard.tsx index 641cae4..570373b 100644 --- a/frontend/src/pages/admin/Dashboard.tsx +++ b/frontend/src/pages/admin/Dashboard.tsx @@ -1,14 +1,16 @@ /** * 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 { useEffect, useState } from 'react'; import { request } from '../../utils/request'; +import { useToast } from '../../contexts/ToastContext'; export function Dashboard() { const [totalUsers, setTotalUsers] = useState(null); const [loading, setLoading] = useState(true); + const toast = useToast(); useEffect(() => { const fetchUserCount = async () => { @@ -19,7 +21,7 @@ export function Dashboard() { setTotalUsers(response.data.total_users); } catch (error) { console.error('Failed to fetch user count:', error); - message.error('无法获取用户总数'); + toast.error('无法获取用户总数'); setTotalUsers(0); // Set to 0 or handle error display } finally { setLoading(false); diff --git a/frontend/src/pages/admin/NASADownload.tsx b/frontend/src/pages/admin/NASADownload.tsx index fa3f26b..f126eec 100644 --- a/frontend/src/pages/admin/NASADownload.tsx +++ b/frontend/src/pages/admin/NASADownload.tsx @@ -10,7 +10,6 @@ import { Checkbox, DatePicker, Button, - message, Badge, Spin, Typography, @@ -31,6 +30,7 @@ import type { Dayjs } from 'dayjs'; import dayjs from 'dayjs'; import isBetween from 'dayjs/plugin/isBetween'; import { request } from '../../utils/request'; +import { useToast } from '../../contexts/ToastContext'; // Extend dayjs with isBetween plugin dayjs.extend(isBetween); @@ -63,6 +63,7 @@ export function NASADownload() { const [loadingDates, setLoadingDates] = useState(false); const [downloading, setDownloading] = useState(false); const [downloadProgress, setDownloadProgress] = useState({ current: 0, total: 0 }); + const toast = useToast(); // Type name mapping const typeNames: Record = { @@ -91,7 +92,7 @@ export function NASADownload() { const { data } = await request.get('/celestial/positions/download/bodies'); setBodies(data.bodies || {}); } catch (error) { - message.error('加载天体列表失败'); + toast.error('加载天体列表失败'); } finally { setLoading(false); } @@ -123,7 +124,7 @@ export function NASADownload() { setAvailableDates(allDates); } catch (error) { - message.error('加载数据状态失败'); + toast.error('加载数据状态失败'); } finally { setLoadingDates(false); } @@ -154,7 +155,7 @@ export function NASADownload() { const handleDownload = async (selectedDate?: Dayjs) => { if (selectedBodies.length === 0) { - message.warning('请先选择至少一个天体'); + toast.warning('请先选择至少一个天体'); return; } @@ -189,10 +190,10 @@ export function NASADownload() { setDownloadProgress({ current: datesToDownload.length, total: datesToDownload.length }); 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(); } else { - message.error('下载失败'); + toast.error('下载失败'); } } else { // Async download for range @@ -200,10 +201,10 @@ export function NASADownload() { body_ids: selectedBodies, dates: datesToDownload }); - message.success('后台下载任务已启动,请前往“系统任务”查看进度'); + toast.success('后台下载任务已启动,请前往“系统任务”查看进度'); } } catch (error) { - message.error('请求失败'); + toast.error('请求失败'); } finally { setDownloading(false); setDownloadProgress({ current: 0, total: 0 }); @@ -240,17 +241,17 @@ export function NASADownload() { const inRange = date.isBetween(dateRange[0], dateRange[1], 'day', '[]'); if (!inRange) { - message.warning('请选择在日期范围内的日期'); + toast.warning('请选择在日期范围内的日期'); return; } if (hasData) { - message.info('该日期已有数据'); + toast.info('该日期已有数据'); return; } if (selectedBodies.length === 0) { - message.warning('请先选择天体'); + toast.warning('请先选择天体'); return; } diff --git a/frontend/src/pages/admin/StaticData.tsx b/frontend/src/pages/admin/StaticData.tsx index 5d4dae9..7052c80 100644 --- a/frontend/src/pages/admin/StaticData.tsx +++ b/frontend/src/pages/admin/StaticData.tsx @@ -2,10 +2,11 @@ * Static Data Management Page */ 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 { DataTable } from '../../components/admin/DataTable'; import { request } from '../../utils/request'; +import { useToast } from '../../contexts/ToastContext'; interface StaticDataItem { id: number; @@ -22,6 +23,7 @@ export function StaticData() { const [isModalOpen, setIsModalOpen] = useState(false); const [editingRecord, setEditingRecord] = useState(null); const [form] = Form.useForm(); + const toast = useToast(); useEffect(() => { loadData(); @@ -34,7 +36,7 @@ export function StaticData() { setData(result.items || []); setFilteredData(result.items || []); } catch (error) { - message.error('加载数据失败'); + toast.error('加载数据失败'); } finally { setLoading(false); } @@ -71,10 +73,10 @@ export function StaticData() { const handleDelete = async (record: StaticDataItem) => { try { await request.delete(`/celestial/static/${record.id}`); - message.success('删除成功'); + toast.success('删除成功'); loadData(); } catch (error) { - message.error('删除失败'); + toast.error('删除失败'); } }; @@ -86,16 +88,16 @@ export function StaticData() { try { values.data = JSON.parse(values.data); } catch (e) { - message.error('JSON格式错误'); + toast.error('JSON格式错误'); return; } if (editingRecord) { await request.put(`/celestial/static/${editingRecord.id}`, values); - message.success('更新成功'); + toast.success('更新成功'); } else { await request.post('/celestial/static', values); - message.success('创建成功'); + toast.success('创建成功'); } setIsModalOpen(false); diff --git a/frontend/src/pages/admin/SystemSettings.tsx b/frontend/src/pages/admin/SystemSettings.tsx index 9d63a3d..1126cc2 100644 --- a/frontend/src/pages/admin/SystemSettings.tsx +++ b/frontend/src/pages/admin/SystemSettings.tsx @@ -2,11 +2,12 @@ * System Settings Management Page */ 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 type { ColumnsType } from 'antd/es/table'; import { DataTable } from '../../components/admin/DataTable'; import { request } from '../../utils/request'; +import { useToast } from '../../contexts/ToastContext'; interface SystemSetting { id: number; @@ -38,6 +39,7 @@ export function SystemSettings() { const [editingRecord, setEditingRecord] = useState(null); const [form] = Form.useForm(); const [clearingCache, setClearingCache] = useState(false); + const toast = useToast(); useEffect(() => { loadData(); @@ -50,7 +52,7 @@ export function SystemSettings() { setData(result.settings || []); setFilteredData(result.settings || []); } catch (error) { - message.error('加载数据失败'); + toast.error('加载数据失败'); } finally { setLoading(false); } @@ -95,10 +97,10 @@ export function SystemSettings() { const handleDelete = async (record: SystemSetting) => { try { await request.delete(`/system/settings/${record.key}`); - message.success('删除成功'); + toast.success('删除成功'); loadData(); } catch (error) { - message.error('删除失败'); + toast.error('删除失败'); } }; @@ -110,11 +112,11 @@ export function SystemSettings() { if (editingRecord) { // Update await request.put(`/system/settings/${editingRecord.key}`, values); - message.success('更新成功'); + toast.success('更新成功'); } else { // Create await request.post('/system/settings', values); - message.success('创建成功'); + toast.success('创建成功'); } setIsModalOpen(false); @@ -129,7 +131,7 @@ export function SystemSettings() { setClearingCache(true); try { const { data } = await request.post('/system/cache/clear'); - message.success( + toast.success( <>
{data.message}
@@ -140,7 +142,7 @@ export function SystemSettings() { ); loadData(); } catch (error) { - message.error('清除缓存失败'); + toast.error('清除缓存失败'); } finally { setClearingCache(false); } diff --git a/frontend/src/pages/admin/Users.tsx b/frontend/src/pages/admin/Users.tsx index e11fe74..c573180 100644 --- a/frontend/src/pages/admin/Users.tsx +++ b/frontend/src/pages/admin/Users.tsx @@ -2,31 +2,28 @@ * User Management Page */ 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 { DataTable } from '../../components/admin/DataTable'; import { request } from '../../utils/request'; -import { ReloadOutlined } from '@ant-design/icons'; +import { DataTable } from '../../components/admin/DataTable'; +import { useToast } from '../../contexts/ToastContext'; interface UserItem { id: number; username: string; - full_name: string; - email: string; + email: string | null; + full_name: string | null; is_active: boolean; roles: string[]; - last_login_at: string; + last_login_at: string | null; created_at: string; } export function Users() { - const [loading, setLoading] = useState(false); const [data, setData] = useState([]); const [filteredData, setFilteredData] = useState([]); - - useEffect(() => { - loadData(); - }, []); + const [loading, setLoading] = useState(false); + const toast = useToast(); const loadData = async () => { setLoading(true); @@ -35,19 +32,23 @@ export function Users() { setData(result.users || []); setFilteredData(result.users || []); } catch (error) { - message.error('加载用户数据失败'); + console.error(error); + toast.error('加载用户数据失败'); } finally { setLoading(false); } }; + useEffect(() => { + loadData(); + }, []); + const handleSearch = (keyword: string) => { const lowerKeyword = keyword.toLowerCase(); - const filtered = data.filter( - (item) => - item.username.toLowerCase().includes(lowerKeyword) || - item.full_name?.toLowerCase().includes(lowerKeyword) || - item.email?.toLowerCase().includes(lowerKeyword) + const filtered = data.filter(item => + item.username.toLowerCase().includes(lowerKeyword) || + (item.email && item.email.toLowerCase().includes(lowerKeyword)) || + (item.full_name && item.full_name.toLowerCase().includes(lowerKeyword)) ); setFilteredData(filtered); }; @@ -55,24 +56,24 @@ export function Users() { const handleStatusChange = async (record: UserItem, checked: boolean) => { try { await request.put(`/users/${record.id}/status`, { is_active: checked }); - message.success(`用户 ${record.username} 状态更新成功`); - - const newData = data.map(item => - item.id === record.id ? { ...item, is_active: checked } : item - ); + toast.success(`用户 ${record.username} 状态更新成功`); + // Update local state + const newData = data.map(item => item.id === record.id ? { ...item, is_active: checked } : item); setData(newData); - setFilteredData(newData); + setFilteredData(newData); // Also update filtered view if needed, simplified here + loadData(); // Reload to be sure } catch (error) { - message.error('状态更新失败'); + console.error(error); + toast.error('状态更新失败'); } }; const handleResetPassword = async (record: UserItem) => { try { await request.post(`/users/${record.id}/reset-password`); - message.success(`用户 ${record.username} 密码已重置`); + toast.success(`用户 ${record.username} 密码已重置`); } catch (error) { - message.error('密码重置失败'); + toast.error('密码重置失败'); } };