From c77061f36dfda5cdfb2838e6d9bc49def5f58b6a Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Thu, 30 Oct 2025 12:54:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B0=86=20IndustryProvider=20(176?= =?UTF-8?q?=E8=A1=8C)=20=E5=AE=8C=E6=95=B4=E8=BF=81=E7=A7=BB=E5=88=B0=20Re?= =?UTF-8?q?dux?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.js | 11 +- src/contexts/IndustryContext.js | 176 ----------------- src/store/index.js | 2 + src/store/slices/industrySlice.js | 178 ++++++++++++++++++ .../Community/components/IndustryCascader.js | 17 +- .../Community/components/UnifiedSearchBox.js | 16 +- 6 files changed, 207 insertions(+), 193 deletions(-) delete mode 100644 src/contexts/IndustryContext.js create mode 100644 src/store/slices/industrySlice.js diff --git a/src/App.js b/src/App.js index 33695b9c..7ff206da 100755 --- a/src/App.js +++ b/src/App.js @@ -48,7 +48,6 @@ import { store } from './store'; import { AuthProvider } from "contexts/AuthContext"; import { AuthModalProvider } from "contexts/AuthModalContext"; import { NotificationProvider, useNotification } from "contexts/NotificationContext"; -import { IndustryProvider } from "contexts/IndustryContext"; // Components import ProtectedRoute from "components/ProtectedRoute"; @@ -321,12 +320,10 @@ export default function App() { - - - - - - + + + + diff --git a/src/contexts/IndustryContext.js b/src/contexts/IndustryContext.js deleted file mode 100644 index 42780420..00000000 --- a/src/contexts/IndustryContext.js +++ /dev/null @@ -1,176 +0,0 @@ -// src/contexts/IndustryContext.js -// 行业分类数据全局上下文 - 使用API获取 + 缓存机制 - -import React, { createContext, useContext, useState, useEffect, useRef } from 'react'; -import { industryData as staticIndustryData } from '../data/industryData'; -import { industryService } from '../services/industryService'; -import { logger } from '../utils/logger'; - -const IndustryContext = createContext(); - -// 缓存配置 -const CACHE_KEY = 'industry_classifications_cache'; -const CACHE_DURATION = 24 * 60 * 60 * 1000; // 1天(24小时) - -/** - * useIndustry Hook - * 在任何组件中使用行业数据 - */ -export const useIndustry = () => { - const context = useContext(IndustryContext); - if (!context) { - throw new Error('useIndustry must be used within IndustryProvider'); - } - return context; -}; - -/** - * 从 localStorage 读取缓存 - */ -const loadFromCache = () => { - try { - const cached = localStorage.getItem(CACHE_KEY); - if (!cached) return null; - - const { data, timestamp } = JSON.parse(cached); - const now = Date.now(); - - // 检查缓存是否过期(1天) - if (now - timestamp > CACHE_DURATION) { - localStorage.removeItem(CACHE_KEY); - logger.debug('IndustryContext', '缓存已过期,清除缓存'); - return null; - } - - logger.debug('IndustryContext', '从缓存加载行业数据', { - count: data?.length || 0, - age: Math.round((now - timestamp) / 1000 / 60) + ' 分钟前' - }); - return data; - } catch (error) { - logger.error('IndustryContext', 'loadFromCache', error); - return null; - } -}; - -/** - * 保存到 localStorage - */ -const saveToCache = (data) => { - try { - localStorage.setItem(CACHE_KEY, JSON.stringify({ - data, - timestamp: Date.now() - })); - logger.debug('IndustryContext', '行业数据已缓存', { - count: data?.length || 0 - }); - } catch (error) { - logger.error('IndustryContext', 'saveToCache', error); - } -}; - -/** - * IndustryProvider 组件 - * 提供全局行业数据管理 - 使用API获取 + 缓存机制 - */ -export const IndustryProvider = ({ children }) => { - const [industryData, setIndustryData] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const hasLoadedRef = useRef(false); - const isLoadingRef = useRef(false); - - /** - * 加载行业数据 - */ - const loadIndustryData = async () => { - // 防止重复加载(处理 StrictMode 双重调用) - if (hasLoadedRef.current || isLoadingRef.current) { - logger.debug('IndustryContext', '跳过重复加载', { - hasLoaded: hasLoadedRef.current, - isLoading: isLoadingRef.current - }); - return industryData; - } - - try { - isLoadingRef.current = true; - setLoading(true); - setError(null); - - logger.debug('IndustryContext', '开始加载行业数据'); - - // 1. 先尝试从缓存加载 - const cachedData = loadFromCache(); - if (cachedData && cachedData.length > 0) { - setIndustryData(cachedData); - hasLoadedRef.current = true; - return cachedData; - } - - // 2. 缓存不存在或过期,调用 API - logger.debug('IndustryContext', '缓存无效,调用API获取数据'); - const response = await industryService.getClassifications(); - - if (response.success && response.data && response.data.length > 0) { - setIndustryData(response.data); - saveToCache(response.data); - hasLoadedRef.current = true; - - logger.debug('IndustryContext', 'API数据加载成功', { - count: response.data.length - }); - - return response.data; - } else { - throw new Error('API返回数据为空'); - } - } catch (err) { - // 3. API 失败,回退到静态数据 - logger.warn('IndustryContext', 'API加载失败,使用静态数据', { - error: err.message - }); - - setError(err.message); - setIndustryData(staticIndustryData); - hasLoadedRef.current = true; - - return staticIndustryData; - } finally { - setLoading(false); - isLoadingRef.current = false; - } - }; - - /** - * 刷新行业数据(清除缓存并重新加载) - */ - const refreshIndustryData = async () => { - logger.debug('IndustryContext', '刷新行业数据,清除缓存'); - localStorage.removeItem(CACHE_KEY); - hasLoadedRef.current = false; - isLoadingRef.current = false; - return loadIndustryData(); - }; - - // 组件挂载时自动加载数据 - useEffect(() => { - loadIndustryData(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const value = { - industryData, - loading, - error, - loadIndustryData, - refreshIndustryData - }; - - return ( - - {children} - - ); -}; diff --git a/src/store/index.js b/src/store/index.js index 33789148..4d2ae544 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -2,12 +2,14 @@ import { configureStore } from '@reduxjs/toolkit'; import communityDataReducer from './slices/communityDataSlice'; import posthogReducer from './slices/posthogSlice'; +import industryReducer from './slices/industrySlice'; import posthogMiddleware from './middleware/posthogMiddleware'; export const store = configureStore({ reducer: { communityData: communityDataReducer, posthog: posthogReducer, // ✅ PostHog Redux 状态管理 + industry: industryReducer, // ✅ 行业分类数据管理 }, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ diff --git a/src/store/slices/industrySlice.js b/src/store/slices/industrySlice.js new file mode 100644 index 00000000..61935bc7 --- /dev/null +++ b/src/store/slices/industrySlice.js @@ -0,0 +1,178 @@ +// src/store/slices/industrySlice.js +// 行业分类数据 Redux Slice - 从 IndustryContext 迁移 + +import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; +import { industryData as staticIndustryData } from '../../data/industryData'; +import { industryService } from '../../services/industryService'; +import { logger } from '../../utils/logger'; + +// 缓存配置 +const CACHE_KEY = 'industry_classifications_cache'; +const CACHE_DURATION = 24 * 60 * 60 * 1000; // 1天(24小时) + +/** + * 从 localStorage 读取缓存 + */ +const loadFromCache = () => { + try { + const cached = localStorage.getItem(CACHE_KEY); + if (!cached) return null; + + const { data, timestamp } = JSON.parse(cached); + const now = Date.now(); + + // 检查缓存是否过期(1天) + if (now - timestamp > CACHE_DURATION) { + localStorage.removeItem(CACHE_KEY); + logger.debug('industrySlice', '缓存已过期,清除缓存'); + return null; + } + + logger.debug('industrySlice', '从缓存加载行业数据', { + count: data?.length || 0, + age: Math.round((now - timestamp) / 1000 / 60) + ' 分钟前' + }); + return data; + } catch (error) { + logger.error('industrySlice', 'loadFromCache', error); + return null; + } +}; + +/** + * 保存到 localStorage + */ +const saveToCache = (data) => { + try { + localStorage.setItem(CACHE_KEY, JSON.stringify({ + data, + timestamp: Date.now() + })); + logger.debug('industrySlice', '行业数据已缓存', { + count: data?.length || 0 + }); + } catch (error) { + logger.error('industrySlice', 'saveToCache', error); + } +}; + +/** + * 异步 Thunk: 加载行业数据 + * 策略:缓存 -> API -> 静态数据 + */ +export const fetchIndustryData = createAsyncThunk( + 'industry/fetchData', + async (_, { rejectWithValue }) => { + try { + logger.debug('industrySlice', '开始加载行业数据'); + + // 1. 先尝试从缓存加载 + const cachedData = loadFromCache(); + if (cachedData && cachedData.length > 0) { + logger.debug('industrySlice', '使用缓存数据', { count: cachedData.length }); + return { data: cachedData, source: 'cache' }; + } + + // 2. 缓存不存在或过期,调用 API + logger.debug('industrySlice', '缓存无效,调用API获取数据'); + const response = await industryService.getClassifications(); + + if (response.success && response.data && response.data.length > 0) { + saveToCache(response.data); + logger.debug('industrySlice', 'API数据加载成功', { + count: response.data.length + }); + return { data: response.data, source: 'api' }; + } else { + throw new Error('API返回数据为空'); + } + } catch (error) { + // 3. API 失败,回退到静态数据 + logger.warn('industrySlice', 'API加载失败,使用静态数据', { + error: error.message + }); + return { data: staticIndustryData, source: 'static', error: error.message }; + } + } +); + +/** + * 异步 Thunk: 刷新行业数据(清除缓存并重新加载) + */ +export const refreshIndustryData = createAsyncThunk( + 'industry/refresh', + async (_, { dispatch }) => { + logger.debug('industrySlice', '刷新行业数据,清除缓存'); + localStorage.removeItem(CACHE_KEY); + return dispatch(fetchIndustryData()); + } +); + +// Industry Slice +const industrySlice = createSlice({ + name: 'industry', + initialState: { + data: null, // 行业数据数组 + loading: false, // 加载状态 + error: null, // 错误信息 + source: null, // 数据来源: 'cache' | 'api' | 'static' + lastFetchTime: null, // 最后加载时间 + }, + reducers: { + // 清除缓存 + clearCache: (state) => { + localStorage.removeItem(CACHE_KEY); + logger.debug('industrySlice', '手动清除缓存'); + }, + // 重置状态 + resetState: (state) => { + state.data = null; + state.loading = false; + state.error = null; + state.source = null; + state.lastFetchTime = null; + }, + }, + extraReducers: (builder) => { + builder + // fetchIndustryData + .addCase(fetchIndustryData.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase(fetchIndustryData.fulfilled, (state, action) => { + state.loading = false; + state.data = action.payload.data; + state.source = action.payload.source; + state.lastFetchTime = Date.now(); + if (action.payload.error) { + state.error = action.payload.error; + } + }) + .addCase(fetchIndustryData.rejected, (state, action) => { + state.loading = false; + state.error = action.error.message; + // 确保总有数据可用 + if (!state.data) { + state.data = staticIndustryData; + state.source = 'static'; + } + }) + // refreshIndustryData + .addCase(refreshIndustryData.pending, (state) => { + state.loading = true; + }); + }, +}); + +// 导出 actions +export const { clearCache, resetState } = industrySlice.actions; + +// 导出 selectors +export const selectIndustryData = (state) => state.industry.data; +export const selectIndustryLoading = (state) => state.industry.loading; +export const selectIndustryError = (state) => state.industry.error; +export const selectIndustrySource = (state) => state.industry.source; + +// 导出 reducer +export default industrySlice.reducer; diff --git a/src/views/Community/components/IndustryCascader.js b/src/views/Community/components/IndustryCascader.js index abd742ec..d5f0f41b 100644 --- a/src/views/Community/components/IndustryCascader.js +++ b/src/views/Community/components/IndustryCascader.js @@ -1,22 +1,25 @@ // src/views/Community/components/IndustryCascader.js -import React, { useState } from 'react'; +import React, { useState, useCallback } from 'react'; import { Card, Form, Cascader } from 'antd'; -import { useIndustry } from '../../../contexts/IndustryContext'; +import { useSelector, useDispatch } from 'react-redux'; +import { fetchIndustryData, selectIndustryData, selectIndustryLoading } from '../../../store/slices/industrySlice'; import { logger } from '../../../utils/logger'; const IndustryCascader = ({ onFilterChange, loading }) => { const [industryCascaderValue, setIndustryCascaderValue] = useState([]); - // 使用全局行业数据 - const { industryData, loadIndustryData, loading: industryLoading } = useIndustry(); + // 使用 Redux 获取行业数据 + const dispatch = useDispatch(); + const industryData = useSelector(selectIndustryData); + const industryLoading = useSelector(selectIndustryLoading); // Cascader 获得焦点时加载数据 - const handleCascaderFocus = async () => { + const handleCascaderFocus = useCallback(async () => { if (!industryData || industryData.length === 0) { logger.debug('IndustryCascader', 'Cascader 获得焦点,开始加载行业数据'); - await loadIndustryData(); + await dispatch(fetchIndustryData()); } - }; + }, [dispatch, industryData]); // Cascader 选择变化 const handleIndustryCascaderChange = (value, selectedOptions) => { diff --git a/src/views/Community/components/UnifiedSearchBox.js b/src/views/Community/components/UnifiedSearchBox.js index 429428b9..1f6c3ed5 100644 --- a/src/views/Community/components/UnifiedSearchBox.js +++ b/src/views/Community/components/UnifiedSearchBox.js @@ -11,7 +11,8 @@ import moment from 'moment'; import dayjs from 'dayjs'; import locale from 'antd/es/date-picker/locale/zh_CN'; import debounce from 'lodash/debounce'; -import { useIndustry } from '../../../contexts/IndustryContext'; +import { useSelector, useDispatch } from 'react-redux'; +import { fetchIndustryData, selectIndustryData, selectIndustryLoading } from '../../../store/slices/industrySlice'; import { stockService } from '../../../services/stockService'; import { logger } from '../../../utils/logger'; import PopularKeywords from './PopularKeywords'; @@ -39,8 +40,17 @@ const UnifiedSearchBox = ({ // ✅ 本地输入状态 - 管理用户的实时输入 const [inputValue, setInputValue] = useState(''); - // 使用全局行业数据 - const { industryData, loadIndustryData, loading: industryLoading } = useIndustry(); + // 使用 Redux 获取行业数据 + const dispatch = useDispatch(); + const industryData = useSelector(selectIndustryData); + const industryLoading = useSelector(selectIndustryLoading); + + // 加载行业数据函数 + const loadIndustryData = useCallback(() => { + if (!industryData) { + dispatch(fetchIndustryData()); + } + }, [dispatch, industryData]); // 搜索触发函数 const triggerSearch = useCallback((params) => {