liuq hai 3 días
pai
achega
f9b6d5f464

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "yunzhu-im",
-  "version": "1.0.11",
+  "version": "1.0.13",
   "main": "./out/main/index.js",
   "author": "example.com",
   "license": "MIT",

+ 30 - 2
src/renderer/src/App.refactored.tsx

@@ -19,7 +19,16 @@ import { getDefaultAvatar, getSessionAvatar } from './utils/avatarUtils'
 import { Contact, Message, SearchResult } from './types'
 
 function App(): JSX.Element {
-  const { isLoggedIn, token, currentUserId, currentUserName, handleLogin, handleLogout } = useAuth()
+  const {
+    isLoggedIn,
+    token,
+    currentUserId,
+    currentUserName,
+    handleLogin,
+    handleLogout,
+    isRestoringSession,
+    sessionExpiredMessage
+  } = useAuth()
   const [activeTab, setActiveTab] = useState<'chat' | 'contact' | 'browser'>('chat')
   const [activeContactId, setActiveContactId] = useState<number | null>(null)
   const activeContactIdRef = useRef(activeContactId)
@@ -550,8 +559,27 @@ function App(): JSX.Element {
     return null
   }
 
+  if (isRestoringSession) {
+    return (
+      <div
+        style={{
+          minHeight: '100vh',
+          width: '100%',
+          display: 'flex',
+          alignItems: 'center',
+          justifyContent: 'center',
+          backgroundColor: '#f5f5f5',
+          color: '#666',
+          fontSize: 15
+        }}
+      >
+        正在登录中...
+      </div>
+    )
+  }
+
   if (!isLoggedIn) {
-    return <Login onLogin={handleLogin} />
+    return <Login onLogin={handleLogin} bannerMessage={sessionExpiredMessage} />
   }
 
   return (

+ 30 - 2
src/renderer/src/App.tsx

@@ -19,7 +19,16 @@ import { getDefaultAvatar, getSessionAvatar } from './utils/avatarUtils'
 import { Contact, Message, SearchResult } from './types'
 
 function App(): JSX.Element {
-  const { isLoggedIn, token, currentUserId, currentUserName, handleLogin, handleLogout } = useAuth()
+  const {
+    isLoggedIn,
+    token,
+    currentUserId,
+    currentUserName,
+    handleLogin,
+    handleLogout,
+    isRestoringSession,
+    sessionExpiredMessage
+  } = useAuth()
   const [activeTab, setActiveTab] = useState<'chat' | 'contact' | 'browser'>('chat')
   const prevActiveTabRef = useRef<'chat' | 'contact' | 'browser'>(activeTab)
   /** 从通讯录详情「发消息」切到消息 tab 时跳过当次 fetchContacts,避免覆盖刚插入的本地会话 */
@@ -660,8 +669,27 @@ function App(): JSX.Element {
     return null
   }
 
+  if (isRestoringSession) {
+    return (
+      <div
+        style={{
+          minHeight: '100vh',
+          width: '100%',
+          display: 'flex',
+          alignItems: 'center',
+          justifyContent: 'center',
+          backgroundColor: '#f5f5f5',
+          color: '#666',
+          fontSize: 15
+        }}
+      >
+        正在登录中...
+      </div>
+    )
+  }
+
   if (!isLoggedIn) {
-    return <Login onLogin={handleLogin} />
+    return <Login onLogin={handleLogin} bannerMessage={sessionExpiredMessage} />
   }
 
   return (

+ 14 - 0
src/renderer/src/components/Login.css

@@ -32,6 +32,20 @@
   box-shadow: 0 1px 3px rgba(0,0,0,0.1);
 }
 
+.login-banner {
+  width: 280px;
+  max-width: calc(100vw - 48px);
+  margin: -24px 0 20px;
+  padding: 10px 12px;
+  font-size: 13px;
+  line-height: 1.4;
+  color: #c0392b;
+  background: #fdecea;
+  border: 1px solid #f5c6cb;
+  border-radius: 6px;
+  text-align: center;
+}
+
 .login-form {
   width: 280px; /* 稍微加宽一点以容纳验证码按钮 */
   display: flex;

+ 9 - 1
src/renderer/src/components/Login.tsx

@@ -7,6 +7,8 @@ import { logger } from '../utils/logger'
 
 interface LoginProps {
   onLogin: (token: string, user?: { id: number; [key: string]: any }) => void
+  /** 会话过期等提示,展示在表单上方 */
+  bannerMessage?: string
 }
 
 interface SavedAccount {
@@ -15,7 +17,7 @@ interface SavedAccount {
   lastLoginTime: number
 }
 
-const Login: React.FC<LoginProps> = ({ onLogin }) => {
+const Login: React.FC<LoginProps> = ({ onLogin, bannerMessage }) => {
   const [loginType, setLoginType] = useState<'password' | 'sms'>('sms')
   const [mobile, setMobile] = useState('')
   const [password, setPassword] = useState('')
@@ -272,6 +274,12 @@ const Login: React.FC<LoginProps> = ({ onLogin }) => {
            <img src={logo} alt="Logo" className="login-avatar" />
         </div>
 
+        {bannerMessage ? (
+          <div className="login-banner" role="alert">
+            {bannerMessage}
+          </div>
+        ) : null}
+
         {/* Tab 切换 */}
         <div className="login-tabs">
           <div 

+ 98 - 73
src/renderer/src/hooks/useAuth.ts

@@ -7,6 +7,12 @@ export function useAuth() {
   const [token, setToken] = useState<string>('')
   const [currentUserId, setCurrentUserId] = useState<number | null>(null)
   const [currentUserName, setCurrentUserName] = useState<string>('')
+  /** 启动时本地有 auth_token,正在请求 /users/me 校验;校验结束前不展示登录表单 */
+  const [isRestoringSession, setIsRestoringSession] = useState(
+    () => typeof localStorage !== 'undefined' && !!localStorage.getItem('auth_token')
+  )
+  /** 会话恢复失败(过期/无效)时在登录页展示的提示 */
+  const [sessionExpiredMessage, setSessionExpiredMessage] = useState('')
 
   // 退出登录函数
   const handleLogout = useCallback(() => {
@@ -22,95 +28,109 @@ export function useAuth() {
     setIsLoggedIn(false)
     setCurrentUserId(null)
     setCurrentUserName('')
+    setSessionExpiredMessage('')
     
     logger.info('App: User logged out')
   }, [])
 
-  // 应用启动时检查是否有保存的 token,尝试自动登录
+  // 应用启动时仅执行一次:若有保存的 token,用 /users/me 校验后再决定进入主界面或登录页
   useEffect(() => {
     const savedToken = localStorage.getItem('auth_token')
     const savedUserId = localStorage.getItem('user_id')
     const savedUserName = localStorage.getItem('user_name')
-    
-    logger.info('useAuth: Checking for saved token on app start', {
-      hasToken: !!savedToken,
-      tokenLength: savedToken?.length || 0,
-      savedUserId,
-      savedUserName,
-      isLoggedIn
-    })
-    
-    if (savedToken && !isLoggedIn) {
-      const validateAndLogin = async () => {
-        logger.info('useAuth: Starting auto login validation', {
-          tokenLength: savedToken.length,
-          tokenPrefix: savedToken.substring(0, 20) + '...'
-        })
-        
-        try {
-          const userInfo = await api.getCurrentUserInfo(savedToken)
-          if (userInfo) {
-            logger.info('useAuth: Auto login validation successful', {
-              userId: userInfo.id,
-              hasUserInfo: true
-            })
-            
-            setToken(savedToken)
-            setIsLoggedIn(true)
-            
-            if (userInfo.id) {
-              setCurrentUserId(userInfo.id)
-              logger.info('useAuth: Set user ID from userInfo', { userId: userInfo.id })
-            } else if (savedUserId) {
-              const userId = parseInt(savedUserId, 10)
-              setCurrentUserId(userId)
-              logger.info('useAuth: Set user ID from saved value', { userId })
-            }
-            
-            const userName = userInfo.name || userInfo.full_name || userInfo.username || userInfo.english_name || savedUserName || ''
-            if (userName) {
-              setCurrentUserName(userName)
-              logger.info('useAuth: Set user name', { userName })
-            }
-            
-            logger.info('useAuth: Auto login successful', { 
-              userId: userInfo.id || savedUserId,
-              userName
-            })
 
-            if (window.electron?.ipcRenderer) {
-              window.electron.ipcRenderer.send('login-success')
-              logger.info('useAuth: Sent login-success to main process (auto login)')
-            }
-          } else {
-            logger.warn('useAuth: Auto login failed, token invalid - no user info returned')
-            localStorage.removeItem('auth_token')
-            localStorage.removeItem('user_id')
-            localStorage.removeItem('user_name')
+    if (!savedToken) {
+      logger.info('useAuth: No saved token on mount, skip session restore')
+      return
+    }
+
+    let cancelled = false
+
+    const validateAndLogin = async () => {
+      logger.info('useAuth: Starting session restore (validate token via /users/me)', {
+        tokenLength: savedToken.length,
+        tokenPrefix: savedToken.substring(0, 20) + '...'
+      })
+
+      try {
+        const userInfo = await api.getCurrentUserInfo(savedToken)
+        if (cancelled) return
+
+        if (userInfo) {
+          setSessionExpiredMessage('')
+          logger.info('useAuth: Session restore validation successful', {
+            userId: userInfo.id,
+            hasUserInfo: true
+          })
+
+          setToken(savedToken)
+          setIsLoggedIn(true)
+
+          if (userInfo.id) {
+            setCurrentUserId(userInfo.id)
+            logger.info('useAuth: Set user ID from userInfo', { userId: userInfo.id })
+          } else if (savedUserId) {
+            const userId = parseInt(savedUserId, 10)
+            setCurrentUserId(userId)
+            logger.info('useAuth: Set user ID from saved value', { userId })
           }
-        } catch (error) {
-          logger.warn('useAuth: Auto login failed, token invalid or expired', {
-            error: error instanceof Error ? {
-              name: error.name,
-              message: error.message,
-              stack: error.stack
-            } : String(error)
+
+          const userName =
+            userInfo.name ||
+            userInfo.full_name ||
+            userInfo.username ||
+            userInfo.english_name ||
+            savedUserName ||
+            ''
+          if (userName) {
+            setCurrentUserName(userName)
+            logger.info('useAuth: Set user name', { userName })
+          }
+
+          logger.info('useAuth: Session restore successful', {
+            userId: userInfo.id || savedUserId,
+            userName
           })
+
+          if (window.electron?.ipcRenderer) {
+            window.electron.ipcRenderer.send('login-success')
+            logger.info('useAuth: Sent login-success to main process (session restore)')
+          }
+        } else {
+          logger.warn('useAuth: Session restore failed, token invalid - no user info returned')
           localStorage.removeItem('auth_token')
           localStorage.removeItem('user_id')
           localStorage.removeItem('user_name')
+          setSessionExpiredMessage('身份已过期,请重新登录')
+        }
+      } catch (error) {
+        if (cancelled) return
+        logger.warn('useAuth: Session restore failed, token invalid or expired', {
+          error:
+            error instanceof Error
+              ? {
+                  name: error.name,
+                  message: error.message,
+                  stack: error.stack
+                }
+              : String(error)
+        })
+        localStorage.removeItem('auth_token')
+        localStorage.removeItem('user_id')
+        localStorage.removeItem('user_name')
+        setSessionExpiredMessage('身份已过期,请重新登录')
+      } finally {
+        if (!cancelled) {
+          setIsRestoringSession(false)
         }
-      }
-      
-      validateAndLogin()
-    } else {
-      if (!savedToken) {
-        logger.info('useAuth: No saved token found, skipping auto login')
-      } else if (isLoggedIn) {
-        logger.info('useAuth: Already logged in, skipping auto login')
       }
     }
-  }, [isLoggedIn])
+
+    validateAndLogin()
+    return () => {
+      cancelled = true
+    }
+  }, [])
 
   // 全局错误处理:当 API 返回 401 时自动退出登录
   useEffect(() => {
@@ -129,6 +149,7 @@ export function useAuth() {
         setIsLoggedIn(false)
         setCurrentUserId(null)
         setCurrentUserName('')
+        setSessionExpiredMessage('登录已过期,请重新登录')
       }
       
       return response
@@ -141,6 +162,8 @@ export function useAuth() {
 
   // 登录处理函数
   const handleLogin = useCallback(async (accessToken: string, user?: { id: number; [key: string]: any }) => {
+    setSessionExpiredMessage('')
+
     logger.info('useAuth: handleLogin called', {
       hasToken: !!accessToken,
       tokenLength: accessToken.length,
@@ -263,6 +286,8 @@ export function useAuth() {
     currentUserId,
     currentUserName,
     handleLogin,
-    handleLogout
+    handleLogout,
+    isRestoringSession,
+    sessionExpiredMessage
   }
 }