refactor: Standardize toast notifications by replacing antd message with ToastContext
parent
241a6cb562
commit
e2a9052e57
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 */}
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
}
|
}
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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('更新失败');
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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('密码重置失败');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue