feat: 调整行业请求数据结构
This commit is contained in:
23
src/App.js
23
src/App.js
@@ -44,6 +44,7 @@ const TradingSimulation = React.lazy(() => import("views/TradingSimulation"));
|
||||
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";
|
||||
@@ -301,16 +302,18 @@ export default function App() {
|
||||
}}
|
||||
>
|
||||
<ErrorBoundary>
|
||||
<AuthProvider>
|
||||
<AuthModalProvider>
|
||||
<NotificationProvider>
|
||||
<AppContent />
|
||||
<AuthModalManager />
|
||||
<NotificationContainer />
|
||||
<NotificationTestTool />
|
||||
</NotificationProvider>
|
||||
</AuthModalProvider>
|
||||
</AuthProvider>
|
||||
<NotificationProvider>
|
||||
<AuthProvider>
|
||||
<AuthModalProvider>
|
||||
<IndustryProvider>
|
||||
<AppContent />
|
||||
<AuthModalManager />
|
||||
<NotificationContainer />
|
||||
<NotificationTestTool />
|
||||
</IndustryProvider>
|
||||
</AuthModalProvider>
|
||||
</AuthProvider>
|
||||
</NotificationProvider>
|
||||
</ErrorBoundary>
|
||||
</ChakraProvider>
|
||||
);
|
||||
|
||||
169
src/contexts/IndustryContext.js
Normal file
169
src/contexts/IndustryContext.js
Normal file
@@ -0,0 +1,169 @@
|
||||
// src/contexts/IndustryContext.js
|
||||
// 行业分类数据全局上下文
|
||||
|
||||
import React, { createContext, useContext, useState, useCallback } from 'react';
|
||||
import { industryService } from '../services/industryService';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
const IndustryContext = createContext();
|
||||
|
||||
// localStorage 缓存配置
|
||||
const CACHE_KEY = 'industry_data';
|
||||
const CACHE_TIME_KEY = 'industry_cache_time';
|
||||
const CACHE_DURATION = 24 * 60 * 60 * 1000; // 1天(毫秒)
|
||||
|
||||
/**
|
||||
* 从 localStorage 读取缓存
|
||||
* @returns {Array|null} 缓存的行业数据,如果无效则返回 null
|
||||
*/
|
||||
const loadFromCache = () => {
|
||||
try {
|
||||
const cachedData = localStorage.getItem(CACHE_KEY);
|
||||
const cacheTime = localStorage.getItem(CACHE_TIME_KEY);
|
||||
|
||||
if (!cachedData || !cacheTime) {
|
||||
logger.debug('IndustryContext', '无缓存数据');
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const cacheAge = now - parseInt(cacheTime, 10);
|
||||
|
||||
if (cacheAge > CACHE_DURATION) {
|
||||
logger.debug('IndustryContext', '缓存已过期', {
|
||||
cacheAge: Math.floor(cacheAge / 1000 / 60), // 分钟
|
||||
maxAge: CACHE_DURATION / 1000 / 60 // 分钟
|
||||
});
|
||||
// 清除过期缓存
|
||||
localStorage.removeItem(CACHE_KEY);
|
||||
localStorage.removeItem(CACHE_TIME_KEY);
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.debug('IndustryContext', '读取缓存成功', {
|
||||
cacheAge: Math.floor(cacheAge / 1000 / 60) // 分钟
|
||||
});
|
||||
|
||||
return JSON.parse(cachedData);
|
||||
} catch (error) {
|
||||
logger.error('IndustryContext', '读取缓存失败', error);
|
||||
// 清除损坏的缓存
|
||||
localStorage.removeItem(CACHE_KEY);
|
||||
localStorage.removeItem(CACHE_TIME_KEY);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存到 localStorage
|
||||
* @param {Array} data - 行业数据
|
||||
*/
|
||||
const saveToCache = (data) => {
|
||||
try {
|
||||
localStorage.setItem(CACHE_KEY, JSON.stringify(data));
|
||||
localStorage.setItem(CACHE_TIME_KEY, Date.now().toString());
|
||||
logger.debug('IndustryContext', '缓存保存成功', {
|
||||
count: data?.length || 0
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('IndustryContext', '缓存保存失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
export const clearIndustryCache = () => {
|
||||
localStorage.removeItem(CACHE_KEY);
|
||||
localStorage.removeItem(CACHE_TIME_KEY);
|
||||
logger.info('IndustryContext', '缓存已清除');
|
||||
};
|
||||
|
||||
/**
|
||||
* IndustryProvider 组件
|
||||
* 提供全局行业数据管理
|
||||
*/
|
||||
export const IndustryProvider = ({ children }) => {
|
||||
const [industryData, setIndustryData] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
/**
|
||||
* 加载行业数据
|
||||
* 优先从缓存读取,缓存无效时调用 API
|
||||
*/
|
||||
const loadIndustryData = useCallback(async () => {
|
||||
// 如果已有数据,不重复加载
|
||||
if (industryData && industryData.length > 0) {
|
||||
logger.debug('IndustryContext', '数据已加载,跳过请求');
|
||||
return industryData;
|
||||
}
|
||||
|
||||
// 尝试从缓存读取
|
||||
const cachedData = loadFromCache();
|
||||
if (cachedData) {
|
||||
setIndustryData(cachedData);
|
||||
return cachedData;
|
||||
}
|
||||
|
||||
// 缓存无效,调用 API
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
logger.debug('IndustryContext', '开始请求行业数据');
|
||||
const response = await industryService.getClassifications();
|
||||
const data = response.data;
|
||||
|
||||
setIndustryData(data);
|
||||
saveToCache(data); // 保存到缓存
|
||||
|
||||
logger.debug('IndustryContext', '行业数据加载成功', {
|
||||
count: data?.length || 0
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (err) {
|
||||
logger.error('IndustryContext', '行业数据加载失败', err);
|
||||
setError(err);
|
||||
return null;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [industryData]);
|
||||
|
||||
/**
|
||||
* 强制刷新数据(清除缓存并重新请求)
|
||||
*/
|
||||
const refreshIndustryData = useCallback(async () => {
|
||||
clearIndustryCache();
|
||||
setIndustryData(null);
|
||||
return await loadIndustryData();
|
||||
}, [loadIndustryData]);
|
||||
|
||||
const value = {
|
||||
industryData, // 行业数据
|
||||
loading, // 加载状态
|
||||
error, // 错误信息
|
||||
loadIndustryData, // 加载数据方法
|
||||
refreshIndustryData // 刷新数据方法
|
||||
};
|
||||
|
||||
return (
|
||||
<IndustryContext.Provider value={value}>
|
||||
{children}
|
||||
</IndustryContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* useIndustry Hook
|
||||
* 在任何组件中使用行业数据
|
||||
*/
|
||||
export const useIndustry = () => {
|
||||
const context = useContext(IndustryContext);
|
||||
if (!context) {
|
||||
throw new Error('useIndustry must be used within IndustryProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
554
src/mocks/data/industries.js
Normal file
554
src/mocks/data/industries.js
Normal file
@@ -0,0 +1,554 @@
|
||||
// src/mocks/data/industries.js
|
||||
// 行业分类完整树形数据 Mock
|
||||
|
||||
/**
|
||||
* 完整的行业分类树形结构
|
||||
* 包含 5 个分类体系,层级深度 2-4 层不等
|
||||
*/
|
||||
export const industryTreeData = [
|
||||
{
|
||||
value: "新财富行业分类",
|
||||
label: "新财富行业分类",
|
||||
children: [
|
||||
{
|
||||
value: "XCF001",
|
||||
label: "传播与文化",
|
||||
children: [
|
||||
{
|
||||
value: "XCF001001",
|
||||
label: "互联网传媒",
|
||||
children: [
|
||||
{ value: "XCF001001001", label: "数字媒体" },
|
||||
{ value: "XCF001001002", label: "社交平台" },
|
||||
{ value: "XCF001001003", label: "短视频平台" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "XCF001002",
|
||||
label: "影视娱乐",
|
||||
children: [
|
||||
{ value: "XCF001002001", label: "电影制作" },
|
||||
{ value: "XCF001002002", label: "网络视频" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "XCF001003",
|
||||
label: "出版发行"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "XCF002",
|
||||
label: "交通运输仓储",
|
||||
children: [
|
||||
{
|
||||
value: "XCF002001",
|
||||
label: "航空运输",
|
||||
children: [
|
||||
{ value: "XCF002001001", label: "航空客运" },
|
||||
{ value: "XCF002001002", label: "航空货运" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "XCF002002",
|
||||
label: "铁路运输"
|
||||
},
|
||||
{
|
||||
value: "XCF002003",
|
||||
label: "公路运输",
|
||||
children: [
|
||||
{ value: "XCF002003001", label: "公路客运" },
|
||||
{ value: "XCF002003002", label: "公路货运" },
|
||||
{ value: "XCF002003003", label: "快递物流" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "XCF003",
|
||||
label: "农林牧渔",
|
||||
children: [
|
||||
{ value: "XCF003001", label: "种植业" },
|
||||
{ value: "XCF003002", label: "林业" },
|
||||
{ value: "XCF003003", label: "畜牧业" },
|
||||
{ value: "XCF003004", label: "渔业" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "XCF004",
|
||||
label: "医药生物",
|
||||
children: [
|
||||
{
|
||||
value: "XCF004001",
|
||||
label: "化学制药",
|
||||
children: [
|
||||
{ value: "XCF004001001", label: "化学原料药" },
|
||||
{ value: "XCF004001002", label: "化学制剂" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "XCF004002",
|
||||
label: "生物制品",
|
||||
children: [
|
||||
{ value: "XCF004002001", label: "疫苗" },
|
||||
{ value: "XCF004002002", label: "血液制品" },
|
||||
{ value: "XCF004002003", label: "诊断试剂" }
|
||||
]
|
||||
},
|
||||
{ value: "XCF004003", label: "中药" },
|
||||
{ value: "XCF004004", label: "医疗器械" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "XCF005",
|
||||
label: "基础化工",
|
||||
children: [
|
||||
{ value: "XCF005001", label: "化学原料" },
|
||||
{ value: "XCF005002", label: "化学制品" },
|
||||
{ value: "XCF005003", label: "塑料" },
|
||||
{ value: "XCF005004", label: "橡胶" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "XCF006",
|
||||
label: "家电",
|
||||
children: [
|
||||
{ value: "XCF006001", label: "白色家电" },
|
||||
{ value: "XCF006002", label: "黑色家电" },
|
||||
{ value: "XCF006003", label: "小家电" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "XCF007",
|
||||
label: "电子",
|
||||
children: [
|
||||
{
|
||||
value: "XCF007001",
|
||||
label: "半导体",
|
||||
children: [
|
||||
{ value: "XCF007001001", label: "芯片设计" },
|
||||
{ value: "XCF007001002", label: "芯片制造" },
|
||||
{ value: "XCF007001003", label: "封装测试" }
|
||||
]
|
||||
},
|
||||
{ value: "XCF007002", label: "元件" },
|
||||
{ value: "XCF007003", label: "光学光电子" },
|
||||
{ value: "XCF007004", label: "消费电子" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "XCF008",
|
||||
label: "计算机",
|
||||
children: [
|
||||
{
|
||||
value: "XCF008001",
|
||||
label: "计算机设备",
|
||||
children: [
|
||||
{ value: "XCF008001001", label: "PC" },
|
||||
{ value: "XCF008001002", label: "服务器" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "XCF008002",
|
||||
label: "软件开发",
|
||||
children: [
|
||||
{ value: "XCF008002001", label: "应用软件" },
|
||||
{ value: "XCF008002002", label: "系统软件" }
|
||||
]
|
||||
},
|
||||
{ value: "XCF008003", label: "IT服务" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "申银万国行业分类",
|
||||
label: "申银万国行业分类",
|
||||
children: [
|
||||
{
|
||||
value: "SW001",
|
||||
label: "电子",
|
||||
children: [
|
||||
{
|
||||
value: "SW001001",
|
||||
label: "半导体",
|
||||
children: [
|
||||
{ value: "SW001001001", label: "半导体材料" },
|
||||
{ value: "SW001001002", label: "半导体设备" },
|
||||
{ value: "SW001001003", label: "集成电路" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "SW001002",
|
||||
label: "电子制造",
|
||||
children: [
|
||||
{ value: "SW001002001", label: "PCB" },
|
||||
{ value: "SW001002002", label: "被动元件" }
|
||||
]
|
||||
},
|
||||
{ value: "SW001003", label: "光学光电子" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "SW002",
|
||||
label: "计算机",
|
||||
children: [
|
||||
{ value: "SW002001", label: "计算机设备" },
|
||||
{ value: "SW002002", label: "计算机应用" },
|
||||
{ value: "SW002003", label: "通信设备" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "SW003",
|
||||
label: "传媒",
|
||||
children: [
|
||||
{ value: "SW003001", label: "互联网传媒" },
|
||||
{ value: "SW003002", label: "营销传播" },
|
||||
{ value: "SW003003", label: "文化传媒" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "SW004",
|
||||
label: "医药生物",
|
||||
children: [
|
||||
{ value: "SW004001", label: "化学制药" },
|
||||
{ value: "SW004002", label: "中药" },
|
||||
{ value: "SW004003", label: "生物制品" },
|
||||
{ value: "SW004004", label: "医疗器械" },
|
||||
{ value: "SW004005", label: "医药商业" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "SW005",
|
||||
label: "汽车",
|
||||
children: [
|
||||
{
|
||||
value: "SW005001",
|
||||
label: "乘用车",
|
||||
children: [
|
||||
{ value: "SW005001001", label: "燃油车" },
|
||||
{ value: "SW005001002", label: "新能源车" }
|
||||
]
|
||||
},
|
||||
{ value: "SW005002", label: "商用车" },
|
||||
{ value: "SW005003", label: "汽车零部件" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "SW006",
|
||||
label: "机械设备",
|
||||
children: [
|
||||
{ value: "SW006001", label: "通用设备" },
|
||||
{ value: "SW006002", label: "专用设备" },
|
||||
{ value: "SW006003", label: "仪器仪表" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "SW007",
|
||||
label: "食品饮料",
|
||||
children: [
|
||||
{ value: "SW007001", label: "白酒" },
|
||||
{ value: "SW007002", label: "啤酒" },
|
||||
{ value: "SW007003", label: "软饮料" },
|
||||
{ value: "SW007004", label: "食品加工" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "SW008",
|
||||
label: "银行",
|
||||
children: [
|
||||
{ value: "SW008001", label: "国有银行" },
|
||||
{ value: "SW008002", label: "股份制银行" },
|
||||
{ value: "SW008003", label: "城商行" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "SW009",
|
||||
label: "非银金融",
|
||||
children: [
|
||||
{ value: "SW009001", label: "证券" },
|
||||
{ value: "SW009002", label: "保险" },
|
||||
{ value: "SW009003", label: "多元金融" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "SW010",
|
||||
label: "房地产",
|
||||
children: [
|
||||
{ value: "SW010001", label: "房地产开发" },
|
||||
{ value: "SW010002", label: "房地产服务" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "证监会行业分类(2001)",
|
||||
label: "证监会行业分类(2001)",
|
||||
children: [
|
||||
{
|
||||
value: "CSRC_A",
|
||||
label: "A 农、林、牧、渔业",
|
||||
children: [
|
||||
{ value: "CSRC_A01", label: "A01 农业" },
|
||||
{ value: "CSRC_A02", label: "A02 林业" },
|
||||
{ value: "CSRC_A03", label: "A03 畜牧业" },
|
||||
{ value: "CSRC_A04", label: "A04 渔业" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "CSRC_B",
|
||||
label: "B 采矿业",
|
||||
children: [
|
||||
{ value: "CSRC_B06", label: "B06 煤炭开采和洗选业" },
|
||||
{ value: "CSRC_B07", label: "B07 石油和天然气开采业" },
|
||||
{ value: "CSRC_B08", label: "B08 黑色金属矿采选业" },
|
||||
{ value: "CSRC_B09", label: "B09 有色金属矿采选业" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "CSRC_C",
|
||||
label: "C 制造业",
|
||||
children: [
|
||||
{
|
||||
value: "CSRC_C13",
|
||||
label: "C13 农副食品加工业",
|
||||
children: [
|
||||
{ value: "CSRC_C1310", label: "C1310 肉制品加工" },
|
||||
{ value: "CSRC_C1320", label: "C1320 水产品加工" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "CSRC_C27",
|
||||
label: "C27 医药制造业",
|
||||
children: [
|
||||
{ value: "CSRC_C2710", label: "C2710 化学药品原料药制造" },
|
||||
{ value: "CSRC_C2720", label: "C2720 化学药品制剂制造" },
|
||||
{ value: "CSRC_C2730", label: "C2730 中药饮片加工" }
|
||||
]
|
||||
},
|
||||
{ value: "CSRC_C35", label: "C35 专用设备制造业" },
|
||||
{ value: "CSRC_C39", label: "C39 计算机、通信和其他电子设备制造业" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "CSRC_I",
|
||||
label: "I 信息传输、软件和信息技术服务业",
|
||||
children: [
|
||||
{ value: "CSRC_I63", label: "I63 电信、广播电视和卫星传输服务" },
|
||||
{ value: "CSRC_I64", label: "I64 互联网和相关服务" },
|
||||
{ value: "CSRC_I65", label: "I65 软件和信息技术服务业" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "CSRC_J",
|
||||
label: "J 金融业",
|
||||
children: [
|
||||
{ value: "CSRC_J66", label: "J66 货币金融服务" },
|
||||
{ value: "CSRC_J67", label: "J67 资本市场服务" },
|
||||
{ value: "CSRC_J68", label: "J68 保险业" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "CSRC_K",
|
||||
label: "K 房地产业",
|
||||
children: [
|
||||
{ value: "CSRC_K70", label: "K70 房地产业" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "中银国际行业分类",
|
||||
label: "中银国际行业分类",
|
||||
children: [
|
||||
{
|
||||
value: "BOC001",
|
||||
label: "能源",
|
||||
children: [
|
||||
{ value: "BOC001001", label: "石油天然气" },
|
||||
{ value: "BOC001002", label: "煤炭" },
|
||||
{ value: "BOC001003", label: "新能源" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "BOC002",
|
||||
label: "原材料",
|
||||
children: [
|
||||
{ value: "BOC002001", label: "化工" },
|
||||
{ value: "BOC002002", label: "钢铁" },
|
||||
{ value: "BOC002003", label: "有色金属" },
|
||||
{ value: "BOC002004", label: "建材" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "BOC003",
|
||||
label: "工业",
|
||||
children: [
|
||||
{ value: "BOC003001", label: "机械" },
|
||||
{ value: "BOC003002", label: "电气设备" },
|
||||
{ value: "BOC003003", label: "国防军工" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "BOC004",
|
||||
label: "消费",
|
||||
children: [
|
||||
{
|
||||
value: "BOC004001",
|
||||
label: "可选消费",
|
||||
children: [
|
||||
{ value: "BOC004001001", label: "汽车" },
|
||||
{ value: "BOC004001002", label: "家电" },
|
||||
{ value: "BOC004001003", label: "纺织服装" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "BOC004002",
|
||||
label: "必需消费",
|
||||
children: [
|
||||
{ value: "BOC004002001", label: "食品饮料" },
|
||||
{ value: "BOC004002002", label: "农林牧渔" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "BOC005",
|
||||
label: "医疗保健",
|
||||
children: [
|
||||
{ value: "BOC005001", label: "医药" },
|
||||
{ value: "BOC005002", label: "医疗器械" },
|
||||
{ value: "BOC005003", label: "医疗服务" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "BOC006",
|
||||
label: "金融",
|
||||
children: [
|
||||
{ value: "BOC006001", label: "银行" },
|
||||
{ value: "BOC006002", label: "非银金融" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "BOC007",
|
||||
label: "科技",
|
||||
children: [
|
||||
{
|
||||
value: "BOC007001",
|
||||
label: "信息技术",
|
||||
children: [
|
||||
{ value: "BOC007001001", label: "半导体" },
|
||||
{ value: "BOC007001002", label: "电子" },
|
||||
{ value: "BOC007001003", label: "计算机" },
|
||||
{ value: "BOC007001004", label: "通信" }
|
||||
]
|
||||
},
|
||||
{ value: "BOC007002", label: "传媒" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "巨潮行业分类",
|
||||
label: "巨潮行业分类",
|
||||
children: [
|
||||
{
|
||||
value: "JC01",
|
||||
label: "制造业",
|
||||
children: [
|
||||
{
|
||||
value: "JC0101",
|
||||
label: "电气机械及器材制造业",
|
||||
children: [
|
||||
{ value: "JC010101", label: "电机制造" },
|
||||
{ value: "JC010102", label: "输配电及控制设备制造" },
|
||||
{ value: "JC010103", label: "电池制造" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "JC0102",
|
||||
label: "医药制造业",
|
||||
children: [
|
||||
{ value: "JC010201", label: "化学药品原药制造" },
|
||||
{ value: "JC010202", label: "化学药品制剂制造" },
|
||||
{ value: "JC010203", label: "中成药制造" },
|
||||
{ value: "JC010204", label: "生物、生化制品制造" }
|
||||
]
|
||||
},
|
||||
{ value: "JC0103", label: "食品制造业" },
|
||||
{ value: "JC0104", label: "纺织业" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "JC02",
|
||||
label: "信息传输、软件和信息技术服务业",
|
||||
children: [
|
||||
{ value: "JC0201", label: "互联网和相关服务" },
|
||||
{ value: "JC0202", label: "软件和信息技术服务业" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "JC03",
|
||||
label: "批发和零售业",
|
||||
children: [
|
||||
{ value: "JC0301", label: "批发业" },
|
||||
{ value: "JC0302", label: "零售业" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "JC04",
|
||||
label: "房地产业",
|
||||
children: [
|
||||
{ value: "JC0401", label: "房地产开发经营" },
|
||||
{ value: "JC0402", label: "物业管理" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "JC05",
|
||||
label: "金融业",
|
||||
children: [
|
||||
{ value: "JC0501", label: "货币金融服务" },
|
||||
{ value: "JC0502", label: "资本市场服务" },
|
||||
{ value: "JC0503", label: "保险业" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "JC06",
|
||||
label: "交通运输、仓储和邮政业",
|
||||
children: [
|
||||
{ value: "JC0601", label: "道路运输业" },
|
||||
{ value: "JC0602", label: "航空运输业" },
|
||||
{ value: "JC0603", label: "水上运输业" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "JC07",
|
||||
label: "采矿业",
|
||||
children: [
|
||||
{ value: "JC0701", label: "煤炭开采和洗选业" },
|
||||
{ value: "JC0702", label: "石油和天然气开采业" },
|
||||
{ value: "JC0703", label: "有色金属矿采选业" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "JC08",
|
||||
label: "农、林、牧、渔业",
|
||||
children: [
|
||||
{ value: "JC0801", label: "农业" },
|
||||
{ value: "JC0802", label: "林业" },
|
||||
{ value: "JC0803", label: "畜牧业" },
|
||||
{ value: "JC0804", label: "渔业" }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: "JC09",
|
||||
label: "建筑业",
|
||||
children: [
|
||||
{ value: "JC0901", label: "房屋建筑业" },
|
||||
{ value: "JC0902", label: "土木工程建筑业" },
|
||||
{ value: "JC0903", label: "建筑装饰和其他建筑业" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -6,6 +6,7 @@ import { accountHandlers } from './account';
|
||||
import { simulationHandlers } from './simulation';
|
||||
import { eventHandlers } from './event';
|
||||
import { paymentHandlers } from './payment';
|
||||
import { industryHandlers } from './industry';
|
||||
|
||||
// 可以在这里添加更多的 handlers
|
||||
// import { userHandlers } from './user';
|
||||
@@ -16,5 +17,6 @@ export const handlers = [
|
||||
...simulationHandlers,
|
||||
...eventHandlers,
|
||||
...paymentHandlers,
|
||||
...industryHandlers,
|
||||
// ...userHandlers,
|
||||
];
|
||||
|
||||
44
src/mocks/handlers/industry.js
Normal file
44
src/mocks/handlers/industry.js
Normal file
@@ -0,0 +1,44 @@
|
||||
// src/mocks/handlers/industry.js
|
||||
// 行业分类相关的 Mock API Handlers
|
||||
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { industryTreeData } from '../data/industries';
|
||||
|
||||
// 模拟网络延迟
|
||||
const delay = (ms = 300) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
export const industryHandlers = [
|
||||
// 获取行业分类完整树形结构
|
||||
http.get('/api/classifications', async ({ request }) => {
|
||||
await delay(500);
|
||||
|
||||
const url = new URL(request.url);
|
||||
const classification = url.searchParams.get('classification');
|
||||
|
||||
console.log('[Mock] 获取行业分类树形数据', { classification });
|
||||
|
||||
try {
|
||||
let data = industryTreeData;
|
||||
|
||||
// 如果指定了分类体系,只返回该体系的数据
|
||||
if (classification) {
|
||||
data = industryTreeData.filter(item => item.value === classification);
|
||||
}
|
||||
|
||||
return HttpResponse.json({
|
||||
success: true,
|
||||
data: data
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[Mock] 获取行业分类失败:', error);
|
||||
return HttpResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: '获取行业分类失败',
|
||||
data: []
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
})
|
||||
];
|
||||
@@ -5,25 +5,25 @@ import axios from 'axios';
|
||||
// 判断当前是否是生产环境
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
|
||||
const API_BASE_URL = getApiBase();
|
||||
|
||||
// 配置 axios 默认包含 credentials
|
||||
axios.defaults.withCredentials = true;
|
||||
|
||||
export const industryService = {
|
||||
// 获取所有行业分类体系
|
||||
async getClassifications() {
|
||||
const res = await axios.get(`${API_BASE_URL}/api/classifications`);
|
||||
return res.data;
|
||||
},
|
||||
// 获取指定体系下的多级行业
|
||||
async getLevels({ classification, level = 1, level1_name, level2_name, level3_name }) {
|
||||
let url = `${API_BASE_URL}/api/levels?classification=${encodeURIComponent(classification)}&level=${level}`;
|
||||
if (level1_name) url += `&level1_name=${encodeURIComponent(level1_name)}`;
|
||||
if (level2_name) url += `&level2_name=${encodeURIComponent(level2_name)}`;
|
||||
if (level3_name) url += `&level3_name=${encodeURIComponent(level3_name)}`;
|
||||
/**
|
||||
* 获取行业分类完整树形结构
|
||||
* @param {string} classification - 可选,指定分类体系名称,不传则返回所有
|
||||
* @returns {Promise} 返回树形结构数据
|
||||
*/
|
||||
async getClassifications(classification) {
|
||||
let url = `${API_BASE_URL}/api/classifications`;
|
||||
if (classification) {
|
||||
url += `?classification=${encodeURIComponent(classification)}`;
|
||||
}
|
||||
const res = await axios.get(url);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
// 注意:getLevels 接口已废弃,使用 getClassifications 替代
|
||||
};
|
||||
@@ -1,10 +1,10 @@
|
||||
// src/views/Community/components/EventFilters.js
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Row, Col, DatePicker, Button, Select, Form, Input } from 'antd';
|
||||
import { Card, Row, Col, DatePicker, Button, Select, Form, Cascader } from 'antd';
|
||||
import { FilterOutlined } from '@ant-design/icons';
|
||||
import moment from 'moment';
|
||||
import locale from 'antd/es/date-picker/locale/zh_CN';
|
||||
import { industryService } from '../../../services/industryService';
|
||||
import { useIndustry } from '../../../contexts/IndustryContext';
|
||||
import { logger } from '../../../utils/logger';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
@@ -12,61 +12,29 @@ const { Option } = Select;
|
||||
|
||||
const EventFilters = ({ filters, onFilterChange, loading }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [industryData, setIndustryData] = useState({
|
||||
classifications: [],
|
||||
level1: [],
|
||||
level2: [],
|
||||
level3: [],
|
||||
level4: []
|
||||
});
|
||||
const [industryCascaderValue, setIndustryCascaderValue] = useState([]);
|
||||
|
||||
// 使用全局行业数据
|
||||
const { industryData, loadIndustryData, loading: industryLoading } = useIndustry();
|
||||
|
||||
// 初始化表单值
|
||||
useEffect(() => {
|
||||
const initialValues = {
|
||||
date_range: filters.date_range ? filters.date_range.split(' 至 ').map(d => moment(d)) : null,
|
||||
sort: filters.sort,
|
||||
importance: filters.importance,
|
||||
industry_classification: filters.industry_classification,
|
||||
industry_code: filters.industry_code
|
||||
importance: filters.importance
|
||||
};
|
||||
form.setFieldsValue(initialValues);
|
||||
}, [filters, form]);
|
||||
|
||||
// 加载行业分类数据
|
||||
const loadIndustryClassifications = async () => {
|
||||
try {
|
||||
const response = await industryService.getClassifications();
|
||||
setIndustryData(prev => ({ ...prev, classifications: response.data }));
|
||||
logger.debug('EventFilters', '行业分类加载成功', {
|
||||
count: response.data?.length || 0
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('EventFilters', 'loadIndustryClassifications', error);
|
||||
// Cascader 获得焦点时加载数据
|
||||
const handleCascaderFocus = async () => {
|
||||
if (!industryData || industryData.length === 0) {
|
||||
logger.debug('EventFilters', 'Cascader 获得焦点,开始加载行业数据');
|
||||
await loadIndustryData();
|
||||
}
|
||||
};
|
||||
|
||||
// 加载行业层级数据
|
||||
const loadIndustryLevels = async (level, params) => {
|
||||
try {
|
||||
const response = await industryService.getLevels(params);
|
||||
setIndustryData(prev => ({ ...prev, [`level${level}`]: response.data }));
|
||||
// 清空下级
|
||||
for (let l = level + 1; l <= 4; l++) {
|
||||
setIndustryData(prev => ({ ...prev, [`level${l}`]: [] }));
|
||||
}
|
||||
logger.debug('EventFilters', '行业层级数据加载成功', {
|
||||
level,
|
||||
count: response.data?.length || 0
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('EventFilters', 'loadIndustryLevels', error, { level, params });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadIndustryClassifications();
|
||||
}, []);
|
||||
|
||||
const handleDateRangeChange = (dates) => {
|
||||
if (dates && dates.length === 2) {
|
||||
const dateRange = `${dates[0].format('YYYY-MM-DD')} 至 ${dates[1].format('YYYY-MM-DD')}`;
|
||||
@@ -84,54 +52,30 @@ const EventFilters = ({ filters, onFilterChange, loading }) => {
|
||||
onFilterChange('importance', value);
|
||||
};
|
||||
|
||||
// 行业分类体系变化时,加载一级行业
|
||||
const handleIndustryClassificationChange = (value) => {
|
||||
form.setFieldsValue({ industry_code: '' });
|
||||
onFilterChange('industry_classification', value);
|
||||
setIndustryData(prev => ({ ...prev, level1: [], level2: [], level3: [], level4: [] }));
|
||||
if (value) {
|
||||
loadIndustryLevels(1, { classification: value, level: 1 });
|
||||
}
|
||||
};
|
||||
// Cascader 选择变化
|
||||
const handleIndustryCascaderChange = (value, selectedOptions) => {
|
||||
setIndustryCascaderValue(value);
|
||||
|
||||
// 级联选择行业
|
||||
const handleLevelChange = (level, value) => {
|
||||
// 直接从state里查找name
|
||||
let name = '';
|
||||
if (level === 1) {
|
||||
const found = industryData.level1.find(item => item.code === value);
|
||||
name = found ? found.name : '';
|
||||
} else if (level === 2) {
|
||||
const found = industryData.level2.find(item => item.code === value);
|
||||
name = found ? found.name : '';
|
||||
} else if (level === 3) {
|
||||
const found = industryData.level3.find(item => item.code === value);
|
||||
name = found ? found.name : '';
|
||||
} else if (level === 4) {
|
||||
const found = industryData.level4.find(item => item.code === value);
|
||||
name = found ? found.name : '';
|
||||
if (value && value.length > 0) {
|
||||
// value[0] = 分类体系名称
|
||||
// value[1...n] = 行业代码(一级~四级)
|
||||
const industryCode = value[value.length - 1]; // 最后一级的 code
|
||||
const classification = value[0]; // 分类体系名称
|
||||
|
||||
onFilterChange('industry_classification', classification);
|
||||
onFilterChange('industry_code', industryCode);
|
||||
|
||||
logger.debug('EventFilters', 'Cascader 选择变化', {
|
||||
value,
|
||||
classification,
|
||||
industryCode,
|
||||
path: selectedOptions.map(o => o.label).join(' > ')
|
||||
});
|
||||
} else {
|
||||
// 清空
|
||||
onFilterChange('industry_classification', '');
|
||||
onFilterChange('industry_code', '');
|
||||
}
|
||||
form.setFieldsValue({ [`level${level}`]: value });
|
||||
form.setFieldsValue({ industry_code: value });
|
||||
onFilterChange('industry_code', value);
|
||||
for (let l = level + 1; l <= 4; l++) {
|
||||
form.setFieldsValue({ [`level${l}`]: undefined });
|
||||
}
|
||||
const params = { classification: form.getFieldValue('industry_classification'), level: level + 1 };
|
||||
if (level === 1) params.level1_name = name;
|
||||
if (level === 2) {
|
||||
params.level1_name = form.getFieldValue('level1_name');
|
||||
params.level2_name = name;
|
||||
}
|
||||
if (level === 3) {
|
||||
params.level1_name = form.getFieldValue('level1_name');
|
||||
params.level2_name = form.getFieldValue('level2_name');
|
||||
params.level3_name = name;
|
||||
}
|
||||
if (level < 4 && name) {
|
||||
loadIndustryLevels(level + 1, params);
|
||||
}
|
||||
form.setFieldsValue({ [`level${level}_name`]: name });
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -176,75 +120,28 @@ const EventFilters = ({ filters, onFilterChange, loading }) => {
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 行业分类级联选择器 - 替换原来的 5 个独立 Select */}
|
||||
<Row gutter={16}>
|
||||
<Col span={6}>
|
||||
<Form.Item label="行业分类" name="industry_classification">
|
||||
<Select
|
||||
placeholder="选择行业分类体系"
|
||||
onChange={handleIndustryClassificationChange}
|
||||
disabled={loading}
|
||||
<Col span={24}>
|
||||
<Form.Item label="行业分类(支持多级联动)">
|
||||
<Cascader
|
||||
options={industryData || []}
|
||||
value={industryCascaderValue}
|
||||
onChange={handleIndustryCascaderChange}
|
||||
onFocus={handleCascaderFocus}
|
||||
changeOnSelect
|
||||
placeholder={industryLoading ? "加载中..." : "请选择行业分类体系和具体行业"}
|
||||
disabled={loading || industryLoading}
|
||||
loading={industryLoading}
|
||||
allowClear
|
||||
>
|
||||
{industryData.classifications.map(item => (
|
||||
<Option key={item.name} value={item.name}>{item.name}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Form.Item label="一级行业" name="level1">
|
||||
<Select
|
||||
placeholder="选择一级行业"
|
||||
onChange={value => handleLevelChange(1, value)}
|
||||
disabled={loading || !form.getFieldValue('industry_classification')}
|
||||
allowClear
|
||||
>
|
||||
{industryData.level1.map(item => (
|
||||
<Option key={item.code} value={item.code} data-name={item.name}>{item.name}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Form.Item label="二级行业" name="level2">
|
||||
<Select
|
||||
placeholder="选择二级行业"
|
||||
onChange={value => handleLevelChange(2, value)}
|
||||
disabled={loading || !form.getFieldValue('level1')}
|
||||
allowClear
|
||||
>
|
||||
{industryData.level2.map(item => (
|
||||
<Option key={item.code} value={item.code} data-name={item.name}>{item.name}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Form.Item label="三级行业" name="level3">
|
||||
<Select
|
||||
placeholder="选择三级行业"
|
||||
onChange={value => handleLevelChange(3, value)}
|
||||
disabled={loading || !form.getFieldValue('level2')}
|
||||
allowClear
|
||||
>
|
||||
{industryData.level3.map(item => (
|
||||
<Option key={item.code} value={item.code} data-name={item.name}>{item.name}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Form.Item label="四级行业" name="level4">
|
||||
<Select
|
||||
placeholder="选择四级行业"
|
||||
onChange={value => handleLevelChange(4, value)}
|
||||
disabled={loading || !form.getFieldValue('level3')}
|
||||
allowClear
|
||||
>
|
||||
{industryData.level4.map(item => (
|
||||
<Option key={item.code} value={item.code} data-name={item.name}>{item.name}</Option>
|
||||
))}
|
||||
</Select>
|
||||
expandTrigger="hover"
|
||||
displayRender={(labels) => labels.join(' > ')}
|
||||
showSearch={{
|
||||
filter: (inputValue, path) =>
|
||||
path.some(option => option.label.toLowerCase().includes(inputValue.toLowerCase()))
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
Reference in New Issue
Block a user