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 { 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;
}

View File

@ -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 {

View File

@ -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<Message[]>([]);
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);
}

View File

@ -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 (
<ToastContext.Provider value={{ showToast, success, error, warning, info }}>
<ToastContext.Provider value={{ showToast, success, error, warning, info, removeToast }}>
{children}
{/* Toast Container - Top Right */}

View File

@ -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 };
}

View File

@ -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);
}

View File

@ -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

View File

@ -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}
/>
)}
</Form>
@ -451,6 +451,7 @@ function ResourceManager({
onDelete,
uploading,
refreshTrigger,
toast,
}: {
bodyId: string;
bodyType: string;
@ -460,6 +461,7 @@ function ResourceManager({
onDelete: (resourceId: number) => Promise<void>;
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('更新失败');
});
}}
/>

View File

@ -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<number | null>(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);

View File

@ -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<string, string> = {
@ -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;
}

View File

@ -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<StaticDataItem | null>(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);

View File

@ -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<SystemSetting | null>(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(
<>
<div>{data.message}</div>
<div style={{ fontSize: 12, color: '#888', marginTop: 4 }}>
@ -140,7 +142,7 @@ export function SystemSettings() {
);
loadData();
} catch (error) {
message.error('清除缓存失败');
toast.error('清除缓存失败');
} finally {
setClearingCache(false);
}

View File

@ -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<UserItem[]>([]);
const [filteredData, setFilteredData] = useState<UserItem[]>([]);
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('密码重置失败');
}
};