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) => {