feat: 将 IndustryProvider (176行) 完整迁移到 Redux
This commit is contained in:
@@ -48,7 +48,6 @@ import { store } from './store';
|
|||||||
import { AuthProvider } from "contexts/AuthContext";
|
import { AuthProvider } from "contexts/AuthContext";
|
||||||
import { AuthModalProvider } from "contexts/AuthModalContext";
|
import { AuthModalProvider } from "contexts/AuthModalContext";
|
||||||
import { NotificationProvider, useNotification } from "contexts/NotificationContext";
|
import { NotificationProvider, useNotification } from "contexts/NotificationContext";
|
||||||
import { IndustryProvider } from "contexts/IndustryContext";
|
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import ProtectedRoute from "components/ProtectedRoute";
|
import ProtectedRoute from "components/ProtectedRoute";
|
||||||
@@ -321,12 +320,10 @@ export default function App() {
|
|||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<AuthModalProvider>
|
<AuthModalProvider>
|
||||||
<IndustryProvider>
|
|
||||||
<AppContent />
|
<AppContent />
|
||||||
<AuthModalManager />
|
<AuthModalManager />
|
||||||
<NotificationContainer />
|
<NotificationContainer />
|
||||||
<NotificationTestTool />
|
<NotificationTestTool />
|
||||||
</IndustryProvider>
|
|
||||||
</AuthModalProvider>
|
</AuthModalProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</NotificationProvider>
|
</NotificationProvider>
|
||||||
|
|||||||
@@ -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 (
|
|
||||||
<IndustryContext.Provider value={value}>
|
|
||||||
{children}
|
|
||||||
</IndustryContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -2,12 +2,14 @@
|
|||||||
import { configureStore } from '@reduxjs/toolkit';
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
import communityDataReducer from './slices/communityDataSlice';
|
import communityDataReducer from './slices/communityDataSlice';
|
||||||
import posthogReducer from './slices/posthogSlice';
|
import posthogReducer from './slices/posthogSlice';
|
||||||
|
import industryReducer from './slices/industrySlice';
|
||||||
import posthogMiddleware from './middleware/posthogMiddleware';
|
import posthogMiddleware from './middleware/posthogMiddleware';
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
communityData: communityDataReducer,
|
communityData: communityDataReducer,
|
||||||
posthog: posthogReducer, // ✅ PostHog Redux 状态管理
|
posthog: posthogReducer, // ✅ PostHog Redux 状态管理
|
||||||
|
industry: industryReducer, // ✅ 行业分类数据管理
|
||||||
},
|
},
|
||||||
middleware: (getDefaultMiddleware) =>
|
middleware: (getDefaultMiddleware) =>
|
||||||
getDefaultMiddleware({
|
getDefaultMiddleware({
|
||||||
|
|||||||
178
src/store/slices/industrySlice.js
Normal file
178
src/store/slices/industrySlice.js
Normal file
@@ -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;
|
||||||
@@ -1,22 +1,25 @@
|
|||||||
// src/views/Community/components/IndustryCascader.js
|
// src/views/Community/components/IndustryCascader.js
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import { Card, Form, Cascader } from 'antd';
|
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';
|
import { logger } from '../../../utils/logger';
|
||||||
|
|
||||||
const IndustryCascader = ({ onFilterChange, loading }) => {
|
const IndustryCascader = ({ onFilterChange, loading }) => {
|
||||||
const [industryCascaderValue, setIndustryCascaderValue] = useState([]);
|
const [industryCascaderValue, setIndustryCascaderValue] = useState([]);
|
||||||
|
|
||||||
// 使用全局行业数据
|
// 使用 Redux 获取行业数据
|
||||||
const { industryData, loadIndustryData, loading: industryLoading } = useIndustry();
|
const dispatch = useDispatch();
|
||||||
|
const industryData = useSelector(selectIndustryData);
|
||||||
|
const industryLoading = useSelector(selectIndustryLoading);
|
||||||
|
|
||||||
// Cascader 获得焦点时加载数据
|
// Cascader 获得焦点时加载数据
|
||||||
const handleCascaderFocus = async () => {
|
const handleCascaderFocus = useCallback(async () => {
|
||||||
if (!industryData || industryData.length === 0) {
|
if (!industryData || industryData.length === 0) {
|
||||||
logger.debug('IndustryCascader', 'Cascader 获得焦点,开始加载行业数据');
|
logger.debug('IndustryCascader', 'Cascader 获得焦点,开始加载行业数据');
|
||||||
await loadIndustryData();
|
await dispatch(fetchIndustryData());
|
||||||
}
|
}
|
||||||
};
|
}, [dispatch, industryData]);
|
||||||
|
|
||||||
// Cascader 选择变化
|
// Cascader 选择变化
|
||||||
const handleIndustryCascaderChange = (value, selectedOptions) => {
|
const handleIndustryCascaderChange = (value, selectedOptions) => {
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import moment from 'moment';
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import locale from 'antd/es/date-picker/locale/zh_CN';
|
import locale from 'antd/es/date-picker/locale/zh_CN';
|
||||||
import debounce from 'lodash/debounce';
|
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 { stockService } from '../../../services/stockService';
|
||||||
import { logger } from '../../../utils/logger';
|
import { logger } from '../../../utils/logger';
|
||||||
import PopularKeywords from './PopularKeywords';
|
import PopularKeywords from './PopularKeywords';
|
||||||
@@ -39,8 +40,17 @@ const UnifiedSearchBox = ({
|
|||||||
// ✅ 本地输入状态 - 管理用户的实时输入
|
// ✅ 本地输入状态 - 管理用户的实时输入
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
|
||||||
// 使用全局行业数据
|
// 使用 Redux 获取行业数据
|
||||||
const { industryData, loadIndustryData, loading: industryLoading } = useIndustry();
|
const dispatch = useDispatch();
|
||||||
|
const industryData = useSelector(selectIndustryData);
|
||||||
|
const industryLoading = useSelector(selectIndustryLoading);
|
||||||
|
|
||||||
|
// 加载行业数据函数
|
||||||
|
const loadIndustryData = useCallback(() => {
|
||||||
|
if (!industryData) {
|
||||||
|
dispatch(fetchIndustryData());
|
||||||
|
}
|
||||||
|
}, [dispatch, industryData]);
|
||||||
|
|
||||||
// 搜索触发函数
|
// 搜索触发函数
|
||||||
const triggerSearch = useCallback((params) => {
|
const triggerSearch = useCallback((params) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user