diff --git a/frontend/REDUX_SETUP.md b/frontend/REDUX_SETUP.md
new file mode 100644
index 0000000..fcdc5bc
--- /dev/null
+++ b/frontend/REDUX_SETUP.md
@@ -0,0 +1,97 @@
+# Redux 全局状态管理设置说明
+
+## 📦 安装依赖
+
+请手动运行以下命令安装 Redux 相关依赖:
+
+```bash
+cd frontend
+npm install @reduxjs/toolkit react-redux
+```
+
+## ✅ 已完成的实现
+
+### 1. Redux Store 结构
+
+**文件**: `frontend/src/store/index.js`
+- 配置了 Redux store
+- 使用 `@reduxjs/toolkit` 的 `configureStore`
+
+**文件**: `frontend/src/store/appSlice.js`
+- 管理全局状态:
+ - `currentUser`: 当前登录用户
+ - `viewingUserId`: 管理员查看的用户ID
+ - `accountId`: 当前选中的账号ID
+ - `accounts`: 当前用户的账号列表
+ - `users`: 用户列表(管理员可见)
+
+### 2. 已更新的组件
+
+#### App.jsx
+- ✅ 使用 `Provider` 包裹整个应用
+- ✅ 使用 Redux hooks (`useSelector`, `useDispatch`)
+- ✅ 用户切换逻辑使用 Redux actions
+
+#### AccountSelector.jsx
+- ✅ 完全使用 Redux 管理账号选择
+- ✅ 用户切换时自动选择第一个 active 账号
+- ✅ 账号切换时自动同步到 Redux store
+
+#### StatsDashboard.jsx
+- ✅ 使用 `useSelector(selectAccountId)` 获取当前账号ID
+- ✅ 当 `accountId` 变化时自动重新加载数据
+
+#### TradeList.jsx
+- ✅ 使用 `useSelector(selectAccountId)` 获取当前账号ID
+- ✅ 当 `accountId` 变化时自动重新加载数据
+
+#### Recommendations.jsx
+- ✅ 使用 `useSelector(selectAccountId)` 获取当前账号ID
+- ✅ 当 `accountId` 变化时自动更新默认下单量
+
+#### ConfigPanel.jsx
+- ✅ 使用 Redux 获取 `accountId`, `currentUser`, `isAdmin` 等
+- ✅ 移除了 localStorage 轮询和事件监听
+- ✅ 当 `accountId` 变化时自动重新加载配置
+
+#### GlobalConfig.jsx
+- ✅ 使用 Redux 获取 `currentUser`, `isAdmin`
+- ✅ 移除了 props 传递
+
+## 🔄 工作流程
+
+### 用户切换流程
+1. 管理员点击切换用户
+2. `App.jsx` 调用 `dispatch(switchUser(userId))`
+3. Redux store 更新 `viewingUserId`
+4. `AccountSelector` 监听到 `viewingUserId` 变化
+5. 自动加载新用户的账号列表
+6. 自动选择第一个 active 账号
+7. Redux store 更新 `accountId`
+8. 所有使用 `useSelector(selectAccountId)` 的组件自动重新渲染
+9. 各组件在 `useEffect` 中检测到 `accountId` 变化,重新加载数据
+
+### 账号切换流程
+1. 用户在 `AccountSelector` 中选择账号
+2. `AccountSelector` 调用 `dispatch(setAccountId(accountId))`
+3. Redux store 更新 `accountId`
+4. 所有使用 `useSelector(selectAccountId)` 的组件自动重新渲染
+5. 各组件在 `useEffect` 中检测到 `accountId` 变化,重新加载数据
+
+## 🎯 优势
+
+1. **统一状态管理**: 所有用户和账号状态都在 Redux store 中
+2. **自动同步**: 切换用户/账号后,所有页面自动更新
+3. **响应式**: 使用 React hooks,状态变化自动触发重新渲染
+4. **易于调试**: Redux DevTools 可以查看所有状态变化
+5. **向后兼容**: 保留了自定义事件 (`ats:account:changed`),确保旧代码仍能工作
+
+## 📝 注意事项
+
+1. **需要安装依赖**: 请运行 `npm install @reduxjs/toolkit react-redux`
+2. **localStorage 同步**: Redux store 会自动同步到 localStorage,确保刷新后状态不丢失
+3. **事件兼容**: 保留了 `ats:account:changed` 事件,但主要使用 Redux 状态管理
+
+## 🚀 下一步
+
+安装依赖后,系统将完全使用 Redux 管理用户和账号状态,切换用户/账号时所有页面会自动同步更新。
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 19b0ea6..700b32f 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react'
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom'
+import { useDispatch, useSelector } from 'react-redux'
import ConfigPanel from './components/ConfigPanel'
import ConfigGuide from './components/ConfigGuide'
import TradeList from './components/TradeList'
@@ -9,77 +10,56 @@ import LogMonitor from './components/LogMonitor'
import AccountSelector from './components/AccountSelector'
import GlobalConfig from './components/GlobalConfig'
import Login from './components/Login'
-import { api, clearAuthToken, clearCurrentAccountId, setCurrentAccountId, getCurrentAccountId } from './services/api'
-
-const VIEWING_USER_ID_KEY = 'ats_viewing_user_id'
+import { api, clearAuthToken } from './services/api'
+import {
+ setCurrentUser,
+ setViewingUserId,
+ setUsers,
+ switchUser,
+ selectCurrentUser,
+ selectViewingUserId,
+ selectUsers,
+ selectIsAdmin,
+ selectEffectiveUserId,
+} from './store/appSlice'
import './App.css'
function App() {
- const [me, setMe] = useState(null)
+ const dispatch = useDispatch()
+ const currentUser = useSelector(selectCurrentUser)
+ const viewingUserId = useSelector(selectViewingUserId)
+ const users = useSelector(selectUsers)
+ const isAdmin = useSelector(selectIsAdmin)
+ const effectiveUserId = useSelector(selectEffectiveUserId)
+
const [checking, setChecking] = useState(true)
- const [users, setUsers] = useState([])
- const [viewingUserId, setViewingUserId] = useState(() => {
- // 管理员:从localStorage读取当前查看的用户ID,默认是当前登录用户
- const stored = localStorage.getItem(VIEWING_USER_ID_KEY)
- if (stored) {
- const parsed = parseInt(stored, 10)
- if (Number.isFinite(parsed) && parsed > 0) return parsed
- }
- return null
- })
const [showUserPopover, setShowUserPopover] = useState(false)
const userPopoverRef = React.useRef(null)
const refreshMe = async () => {
try {
const u = await api.me()
- setMe(u)
+ dispatch(setCurrentUser(u))
- const isAdmin = (u?.role || '') === 'admin'
+ const isAdminValue = (u?.role || '') === 'admin'
// 管理员:加载用户列表
- if (isAdmin) {
+ if (isAdminValue) {
try {
const userList = await api.getUsers()
- setUsers(Array.isArray(userList) ? userList : [])
+ const usersArray = Array.isArray(userList) ? userList : []
+ dispatch(setUsers(usersArray))
// 如果viewingUserId未设置或不在列表中,设置为当前登录用户
const currentUserId = parseInt(String(u?.id || ''), 10)
- if (!viewingUserId || !userList.some((user) => parseInt(String(user?.id || ''), 10) === viewingUserId)) {
- setViewingUserId(currentUserId)
- localStorage.setItem(VIEWING_USER_ID_KEY, String(currentUserId))
+ if (!viewingUserId || !usersArray.some((user) => parseInt(String(user?.id || ''), 10) === viewingUserId)) {
+ dispatch(setViewingUserId(currentUserId))
}
} catch (e) {
console.error('加载用户列表失败:', e)
}
}
-
- // 登录后默认选择账号(避免 admin/用户切换时沿用旧 accountId)
- try {
- const list = await api.getAccounts()
- const accounts = Array.isArray(list) ? list : []
- const active = accounts.filter((a) => String(a?.status || 'active') === 'active')
-
- let target = null
- if (isAdmin) {
- // 管理员:默认选第一个 active 账号(避免沿用旧值导致“看起来还是上个账号”)
- target = active[0]?.id || accounts[0]?.id
- } else {
- // 普通用户:优先选“自己的账号”(且必须 active),否则选第一个 active
- const uid = parseInt(String(u?.id || ''), 10)
- const match = active.find((a) => parseInt(String(a?.id || ''), 10) === uid)
- target = match?.id || active[0]?.id || accounts[0]?.id
- }
-
- if (target) {
- const cur = getCurrentAccountId()
- const next = parseInt(String(target), 10)
- if (Number.isFinite(next) && next > 0 && cur !== next) setCurrentAccountId(next)
- }
- } catch (e) {
- // ignore
- }
} catch (e) {
- setMe(null)
+ dispatch(setCurrentUser(null))
} finally {
setChecking(false)
}
@@ -98,20 +78,15 @@ function App() {
}
}, [showUserPopover])
- // 必须在定义 effectiveUserId 之前定义 isAdmin
- const isAdmin = (me?.role || '') === 'admin'
-
// 当前查看的用户信息
const currentViewingUser = users.find((u) => parseInt(String(u?.id || ''), 10) === viewingUserId)
- const effectiveUserId = isAdmin && viewingUserId ? viewingUserId : parseInt(String(me?.id || ''), 10)
const handleSwitchUser = (userId) => {
const nextUserId = parseInt(String(userId || ''), 10)
if (Number.isFinite(nextUserId) && nextUserId > 0) {
- setViewingUserId(nextUserId)
- localStorage.setItem(VIEWING_USER_ID_KEY, String(nextUserId))
+ dispatch(switchUser(nextUserId))
setShowUserPopover(false)
- // 切换用户后,触发account变化事件,让AccountSelector重新加载该用户的账号列表
+ // 触发自定义事件,保持向后兼容
window.dispatchEvent(new CustomEvent('ats:user:switched', { detail: { userId: nextUserId } }))
}
}
@@ -131,7 +106,7 @@ function App() {
)
}
- if (!me) {
+ if (!currentUser) {
return