diff --git a/web/src/components/LoginPage.tsx b/web/src/components/LoginPage.tsx index 65c0aa02..a7d04d1a 100644 --- a/web/src/components/LoginPage.tsx +++ b/web/src/components/LoginPage.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import { useNavigate } from 'react-router-dom' import { useAuth } from '../contexts/AuthContext' import { useLanguage } from '../contexts/LanguageContext' @@ -24,6 +24,18 @@ export function LoginPage() { const adminMode = false const { config: systemConfig } = useSystemConfig() const registrationEnabled = systemConfig?.registration_enabled !== false + const [expiredToastId, setExpiredToastId] = useState(null) + + // Show notification if user was redirected here due to 401 + useEffect(() => { + if (sessionStorage.getItem('from401') === 'true') { + const id = toast.warning(t('sessionExpired', language), { + duration: Infinity // Keep showing until user dismisses or logs in + }) + setExpiredToastId(id) + sessionStorage.removeItem('from401') + } + }, [language]) const handleAdminLogin = async (e: React.FormEvent) => { e.preventDefault() @@ -34,6 +46,11 @@ export function LoginPage() { const msg = result.message || t('loginFailed', language) setError(msg) toast.error(msg) + } else { + // Dismiss the "login expired" toast on successful login + if (expiredToastId) { + toast.dismiss(expiredToastId) + } } setLoading(false) } @@ -49,6 +66,11 @@ export function LoginPage() { if (result.requiresOTP && result.userID) { setUserID(result.userID) setStep('otp') + } else { + // Dismiss the "login expired" toast on successful login (no OTP required) + if (expiredToastId) { + toast.dismiss(expiredToastId) + } } } else { const msg = result.message || t('loginFailed', language) @@ -70,6 +92,11 @@ export function LoginPage() { const msg = result.message || t('verificationFailed', language) setError(msg) toast.error(msg) + } else { + // Dismiss the "login expired" toast on successful OTP verification + if (expiredToastId) { + toast.dismiss(expiredToastId) + } } // 成功的话AuthContext会自动处理登录状态 diff --git a/web/src/components/TraderConfigModal.tsx b/web/src/components/TraderConfigModal.tsx index 30e104a6..2fa53a7f 100644 --- a/web/src/components/TraderConfigModal.tsx +++ b/web/src/components/TraderConfigModal.tsx @@ -4,6 +4,7 @@ import { useLanguage } from '../contexts/LanguageContext' import { t } from '../i18n/translations' import { toast } from 'sonner' import { Pencil, Plus, X as IconX } from 'lucide-react' +import { httpClient } from '../lib/httpClient' // 提取下划线后面的名称部分 function getShortName(fullName: string): string { @@ -114,7 +115,7 @@ export function TraderConfigModal({ useEffect(() => { const fetchConfig = async () => { try { - const response = await fetch('/api/config') + const response = await httpClient.get('/api/config') const config = await response.json() if (config.default_coins) { setAvailableCoins(config.default_coins) @@ -140,7 +141,7 @@ export function TraderConfigModal({ useEffect(() => { const fetchPromptTemplates = async () => { try { - const response = await fetch('/api/prompt-templates') + const response = await httpClient.get('/api/prompt-templates') const data = await response.json() if (data.templates) { setPromptTemplates(data.templates) @@ -198,19 +199,13 @@ export function TraderConfigModal({ throw new Error('未登录,请先登录') } - const response = await fetch( + const response = await httpClient.get( `/api/account?trader_id=${traderData.trader_id}`, { - headers: { - Authorization: `Bearer ${token}`, - }, + Authorization: `Bearer ${token}`, } ) - if (!response.ok) { - throw new Error('获取账户余额失败') - } - const data = await response.json() // total_equity = 当前账户净值(包含未实现盈亏) diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts index 9d17ace9..26f7102a 100644 --- a/web/src/i18n/translations.ts +++ b/web/src/i18n/translations.ts @@ -474,6 +474,7 @@ export const translations = { registrationFailed: 'Registration failed. Please try again.', verificationFailed: 'OTP verification failed. Please check the code and try again.', + sessionExpired: 'Session expired, please login again', invalidCredentials: 'Invalid email or password', weak: 'Weak', medium: 'Medium', @@ -1287,6 +1288,7 @@ export const translations = { loginFailed: '登录失败,请检查您的邮箱和密码。', registrationFailed: '注册失败,请重试。', verificationFailed: 'OTP 验证失败,请检查验证码后重试。', + sessionExpired: '登录已过期,请重新登录', invalidCredentials: '邮箱或密码错误', weak: '弱', medium: '中', diff --git a/web/src/lib/httpClient.ts b/web/src/lib/httpClient.ts index 15ebc16c..079e6bdf 100644 --- a/web/src/lib/httpClient.ts +++ b/web/src/lib/httpClient.ts @@ -6,10 +6,9 @@ * - Automatic 401 token expiration handling * - Auth state cleanup on unauthorized * - Automatic redirect to login page + * - Notification shown on login page after redirect */ -import { toast } from 'sonner' - export class HttpClient { // Singleton flag to prevent duplicate 401 handling private static isHandling401 = false @@ -21,13 +20,6 @@ export class HttpClient { HttpClient.isHandling401 = false } - /** - * Show login required notification to user - */ - private showLoginRequiredNotification(): void { - toast.warning('登录已过期,请先登录', { duration: 1800 }) - } - /** * Response interceptor - handles common HTTP errors * @@ -53,23 +45,24 @@ export class HttpClient { // Notify global listeners (AuthContext will react to this) window.dispatchEvent(new Event('unauthorized')) - // Show user-friendly notification (only once) - this.showLoginRequiredNotification() - - // Delay redirect to let user see the notification - setTimeout(() => { - // Only redirect if not already on login page - if (!window.location.pathname.includes('/login')) { - // Save current location for post-login redirect - const returnUrl = window.location.pathname + window.location.search - if (returnUrl !== '/login' && returnUrl !== '/') { - sessionStorage.setItem('returnUrl', returnUrl) - } - - window.location.href = '/login' + // Only redirect if not already on login page + if (!window.location.pathname.includes('/login')) { + // Save current location for post-login redirect + const returnUrl = window.location.pathname + window.location.search + if (returnUrl !== '/login' && returnUrl !== '/') { + sessionStorage.setItem('returnUrl', returnUrl) } - // Note: No need to reset flag since we're redirecting - }, 1500) // 1.5秒延迟,让用户看到提示 + + // Mark that user came from 401 (login page will show notification) + sessionStorage.setItem('from401', 'true') + + // Redirect immediately to login page + window.location.href = '/login' + + // Return pending promise to prevent error from being caught by SWR/React + // The notification will be shown on the login page + return new Promise(() => {}) as Promise + } throw new Error('登录已过期,请重新登录') }