diff --git a/backend/src/services/marketService.ts b/backend/src/services/marketService.ts index 64ef3d2..75435e2 100644 --- a/backend/src/services/marketService.ts +++ b/backend/src/services/marketService.ts @@ -14,188 +14,286 @@ export const fetchMarketOverview = async () => { try { // 获取数据源配置 const dataSourceConfig = getDataSourceConfig(); - // 使用TQSDK数据源 - const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig); - const overview = await dataSource.getMarketOverview(); - // 转换为前端需要的格式 - return overview.map(item => ({ - code: item.symbol, - name: item.name, - currentPrice: item.price, - changePercent: item.change_percent, - winRate: Math.floor(Math.random() * 50) + 30, // 模拟胜率 - atr: +(Math.random() * 5 + 0.5).toFixed(2), // 模拟ATR - adx: Math.floor(Math.random() * 60) + 10, // 模拟ADX - adxStatus: adx => { - if (adx < 20) return '无趋势/震荡'; - if (adx < 40) return '弱趋势'; - return '强趋势'; - }, - trends: { - '5MIN': { - direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], - status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], - rsi: Math.floor(Math.random() * 80) + 10 - }, - '30MIN': { - direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], - status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], - rsi: Math.floor(Math.random() * 80) + 10 - }, - '1HOUR': { - direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], - status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], - rsi: Math.floor(Math.random() * 80) + 10 - }, - '1DAY': { - direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], - status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], - rsi: Math.floor(Math.random() * 80) + 10 + // 检查是否有可用的数据源 + const hasAvailableDataSource = dataSourceConfig.tqsdk?.enabled || dataSourceConfig.test?.enabled; + if (!hasAvailableDataSource) { + throw new Error('无可用数据源,请在管理配置中启用至少一个数据源'); + } + + // 尝试使用TQSDK数据源 + if (dataSourceConfig.tqsdk?.enabled) { + try { + const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig); + + // 使用用户指定的合约列表 + const overview = []; + for (const future of futuresList) { + try { + // 构建合约符号 + const symbol = `${future.exchange}.${future.code}${new Date().getFullYear().toString().slice(-2)}05`; + // 获取合约详情和实时行情 + const tick = await dataSource.getTickData(symbol); + + overview.push({ + code: future.code, + name: future.name, + currentPrice: tick.last_price, + changePercent: tick.price_change / tick.pre_close * 100, + winRate: Math.floor(Math.random() * 50) + 30, // 模拟胜率 + atr: +(Math.random() * 5 + 0.5).toFixed(2), // 模拟ATR + adx: Math.floor(Math.random() * 60) + 10, // 模拟ADX + adxStatus: adx => { + if (adx < 20) return '无趋势/震荡'; + if (adx < 40) return '弱趋势'; + return '强趋势'; + }, + trends: { + '5MIN': { + direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], + status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], + rsi: Math.floor(Math.random() * 80) + 10 + }, + '30MIN': { + direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], + status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], + rsi: Math.floor(Math.random() * 80) + 10 + }, + '1HOUR': { + direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], + status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], + rsi: Math.floor(Math.random() * 80) + 10 + }, + '1DAY': { + direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], + status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], + rsi: Math.floor(Math.random() * 80) + 10 + } + }, + tradingAdvice: { + entry: tick.last_price, + stopLoss: tick.last_price * (1 - 0.02 * (Math.random() + 0.5)), + target: tick.last_price * (1 + 0.03 * (Math.random() + 0.5)), + resistance: tick.last_price * (1 + 0.05 * (Math.random() + 0.5)), + support: tick.last_price * (1 - 0.05 * (Math.random() + 0.5)) + }, + overallView: ['观望', '中线', '多头排列', '空头排列', '震荡'][Math.floor(Math.random() * 5)], + aiAnalysis: `MACD:金叉向上 | RSI:${Math.floor(Math.random() * 80) + 10}(中性) | 布林带:中轨附近` + }); + } catch (error) { + console.error(`获取合约${future.code}行情失败:`, error); + // 跳过获取失败的合约 + continue; + } } - }, - tradingAdvice: { - entry: item.price, - stopLoss: item.price * (1 - 0.02 * (Math.random() + 0.5)), - target: item.price * (1 + 0.03 * (Math.random() + 0.5)), - resistance: item.price * (1 + 0.05 * (Math.random() + 0.5)), - support: item.price * (1 - 0.05 * (Math.random() + 0.5)) - }, - overallView: ['观望', '中线', '多头排列', '空头排列', '震荡'][Math.floor(Math.random() * 5)], - aiAnalysis: `MACD:金叉向上 | RSI:${Math.floor(Math.random() * 80) + 10}(中性) | 布林带:中轨附近` - })); - } catch (error) { - console.error('获取市场概览失败:', error); - // 获取数据源配置 - const dataSourceConfig = getDataSourceConfig(); - // 检查是否启用了测试数据源 - if (dataSourceConfig.test && dataSourceConfig.test.enabled) { - // 启用了测试数据源,使用测试数据 + + return overview; + } catch (error) { + console.error('TQSDK数据源获取失败:', error); + // TQSDK数据源失败,尝试使用测试数据源 + if (dataSourceConfig.test?.enabled) { + console.log('切换到测试数据源'); + // 启用了测试数据源,使用测试数据 + await new Promise(resolve => setTimeout(resolve, 300)); + return generateFuturesOverview(); + } else { + // 未启用测试数据源,返回友好的错误提示 + throw new Error('获取市场概览失败,所有数据源均不可用'); + } + } + } else if (dataSourceConfig.test?.enabled) { + // 直接使用测试数据源 + console.log('使用测试数据源'); await new Promise(resolve => setTimeout(resolve, 300)); return generateFuturesOverview(); } else { - // 未启用测试数据源,返回友好的错误提示 - throw new Error('获取市场概览失败,请检查数据源配置'); + // 无可用数据源 + throw new Error('无可用数据源,请在管理配置中启用至少一个数据源'); } + } catch (error) { + console.error('获取市场概览失败:', error); + // 直接返回友好的错误提示 + throw new Error(error instanceof Error ? error.message : '获取市场概览失败,请检查数据源配置'); } }; // 获取品种详情 export const fetchMarketDetail = async (symbol: string) => { try { + // 查找合约信息 + const future = futuresList.find(item => item.code === symbol); + if (!future) { + throw new Error('品种不存在'); + } + // 获取数据源配置 const dataSourceConfig = getDataSourceConfig(); - // 使用TQSDK数据源 - const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig); - const contract = await dataSource.getContractDetail(symbol); - const tick = await dataSource.getTickData(symbol); - // 转换为前端需要的格式 - return { - code: contract.symbol, - name: contract.name, - fullName: `${contract.name}-${contract.symbol}605`, - currentPrice: tick.last_price, - changePercent: tick.price_change / tick.pre_close * 100, - winRate: Math.floor(Math.random() * 50) + 30, - atr: +(Math.random() * 5 + 0.5).toFixed(2), - adx: Math.floor(Math.random() * 60) + 10, - adxStatus: adx => { - if (adx < 20) return '无趋势/震荡'; - if (adx < 40) return '弱趋势'; - return '强趋势'; - }, - trends: { - '5MIN': { - direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], - status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], - rsi: Math.floor(Math.random() * 80) + 10 - }, - '30MIN': { - direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], - status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], - rsi: Math.floor(Math.random() * 80) + 10 - }, - '1HOUR': { - direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], - status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], - rsi: Math.floor(Math.random() * 80) + 10 - }, - '1DAY': { - direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], - status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], - rsi: Math.floor(Math.random() * 80) + 10 + // 检查是否有可用的数据源 + const hasAvailableDataSource = dataSourceConfig.tqsdk?.enabled || dataSourceConfig.test?.enabled; + if (!hasAvailableDataSource) { + throw new Error('无可用数据源,请在管理配置中启用至少一个数据源'); + } + + // 尝试使用TQSDK数据源 + if (dataSourceConfig.tqsdk?.enabled) { + try { + const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig); + + // 构建合约符号 + const contractSymbol = `${future.exchange}.${future.code}${new Date().getFullYear().toString().slice(-2)}05`; + + // 获取合约详情和实时行情 + const tick = await dataSource.getTickData(contractSymbol); + + // 转换为前端需要的格式 + return { + code: future.code, + name: future.name, + fullName: `${future.name}-${future.code}605`, + currentPrice: tick.last_price, + changePercent: tick.price_change / tick.pre_close * 100, + winRate: Math.floor(Math.random() * 50) + 30, + atr: +(Math.random() * 5 + 0.5).toFixed(2), + adx: Math.floor(Math.random() * 60) + 10, + adxStatus: adx => { + if (adx < 20) return '无趋势/震荡'; + if (adx < 40) return '弱趋势'; + return '强趋势'; + }, + trends: { + '5MIN': { + direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], + status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], + rsi: Math.floor(Math.random() * 80) + 10 + }, + '30MIN': { + direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], + status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], + rsi: Math.floor(Math.random() * 80) + 10 + }, + '1HOUR': { + direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], + status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], + rsi: Math.floor(Math.random() * 80) + 10 + }, + '1DAY': { + direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], + status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], + rsi: Math.floor(Math.random() * 80) + 10 + } + }, + indicators: { + macd: ['金叉向上', '死叉向下', '走平'][Math.floor(Math.random() * 3)], + rsi: `${Math.floor(Math.random() * 80) + 10}(中性)`, + bollinger: ['触及上轨', '触及下轨', '中轨附近'][Math.floor(Math.random() * 3)], + kdj: ['金叉向上', '死叉向下', '走平'][Math.floor(Math.random() * 3)] + }, + tradingAdvice: { + entry: tick.last_price, + stopLoss: tick.last_price * (1 - 0.02 * (Math.random() + 0.5)), + target: tick.last_price * (1 + 0.03 * (Math.random() + 0.5)), + resistance: tick.last_price * (1 + 0.05 * (Math.random() + 0.5)), + support: tick.last_price * (1 - 0.05 * (Math.random() + 0.5)) + }, + riskLevel: ['低', '中等', '高'][Math.floor(Math.random() * 3)], + volatility: ['低', '中等', '高'][Math.floor(Math.random() * 3)], + overallView: ['观望', '中线', '多头排列', '空头排列', '震荡'][Math.floor(Math.random() * 5)], + aiAnalysis: `MACD:金叉向上 | RSI:${Math.floor(Math.random() * 80) + 10}(中性) | 布林带:中轨附近` + }; + } catch (error) { + console.error('TQSDK数据源获取失败:', error); + // TQSDK数据源失败,尝试使用测试数据源 + if (dataSourceConfig.test?.enabled) { + console.log('切换到测试数据源'); + // 启用了测试数据源,使用测试数据 + await new Promise(resolve => setTimeout(resolve, 200)); + return generateFutureData(symbol, future.name); + } else { + // 未启用测试数据源,返回友好的错误提示 + throw new Error('获取品种详情失败,所有数据源均不可用'); } - }, - indicators: { - macd: ['金叉向上', '死叉向下', '走平'][Math.floor(Math.random() * 3)], - rsi: `${Math.floor(Math.random() * 80) + 10}(中性)`, - bollinger: ['触及上轨', '触及下轨', '中轨附近'][Math.floor(Math.random() * 3)], - kdj: ['金叉向上', '死叉向下', '走平'][Math.floor(Math.random() * 3)] - }, - tradingAdvice: { - entry: tick.last_price, - stopLoss: tick.last_price * (1 - 0.02 * (Math.random() + 0.5)), - target: tick.last_price * (1 + 0.03 * (Math.random() + 0.5)), - resistance: tick.last_price * (1 + 0.05 * (Math.random() + 0.5)), - support: tick.last_price * (1 - 0.05 * (Math.random() + 0.5)) - }, - riskLevel: ['低', '中等', '高'][Math.floor(Math.random() * 3)], - volatility: ['低', '中等', '高'][Math.floor(Math.random() * 3)], - overallView: ['观望', '中线', '多头排列', '空头排列', '震荡'][Math.floor(Math.random() * 5)], - aiAnalysis: `MACD:金叉向上 | RSI:${Math.floor(Math.random() * 80) + 10}(中性) | 布林带:中轨附近` - }; - } catch (error) { - console.error(`获取品种${symbol}详情失败:`, error); - // 获取数据源配置 - const dataSourceConfig = getDataSourceConfig(); - // 检查是否启用了测试数据源 - if (dataSourceConfig.test && dataSourceConfig.test.enabled) { - // 启用了测试数据源,使用测试数据 - await new Promise(resolve => setTimeout(resolve, 200)); - const future = futuresList.find(item => item.code === symbol); - if (!future) { - throw new Error('品种不存在'); } + } else if (dataSourceConfig.test?.enabled) { + // 直接使用测试数据源 + console.log('使用测试数据源'); + await new Promise(resolve => setTimeout(resolve, 200)); return generateFutureData(symbol, future.name); } else { - // 未启用测试数据源,返回友好的错误提示 - throw new Error('获取品种详情失败,请检查数据源配置'); + // 无可用数据源 + throw new Error('无可用数据源,请在管理配置中启用至少一个数据源'); } + } catch (error) { + console.error(`获取品种${symbol}详情失败:`, error); + // 直接返回友好的错误提示 + throw new Error(error instanceof Error ? error.message : '获取品种详情失败,请检查数据源配置'); } }; // 获取K线数据 export const fetchKlineData = async (symbol: string, period: string) => { try { - // 获取数据源配置 - const dataSourceConfig = getDataSourceConfig(); - // 使用TQSDK数据源 - const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig); - const klineData = await dataSource.getKlineData(symbol, period, 30); + // 查找合约信息 + const future = futuresList.find(item => item.code === symbol); + if (!future) { + throw new Error('品种不存在'); + } - // 转换为前端需要的格式 - return klineData.map(item => ({ - timestamp: item.datetime / 1000000000, // 转换为秒 - open: item.open, - high: item.high, - low: item.low, - close: item.close, - volume: item.volume - })); - } catch (error) { - console.error(`获取合约${symbol}K线数据失败:`, error); // 获取数据源配置 const dataSourceConfig = getDataSourceConfig(); - // 检查是否启用了测试数据源 - if (dataSourceConfig.test && dataSourceConfig.test.enabled) { - // 启用了测试数据源,使用测试数据 + + // 检查是否有可用的数据源 + const hasAvailableDataSource = dataSourceConfig.tqsdk?.enabled || dataSourceConfig.test?.enabled; + if (!hasAvailableDataSource) { + throw new Error('无可用数据源,请在管理配置中启用至少一个数据源'); + } + + // 尝试使用TQSDK数据源 + if (dataSourceConfig.tqsdk?.enabled) { + try { + const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig); + + // 构建合约符号 + const contractSymbol = `${future.exchange}.${future.code}${new Date().getFullYear().toString().slice(-2)}05`; + + // 获取K线数据 + const klineData = await dataSource.getKlineData(contractSymbol, period, 30); + + // 转换为前端需要的格式 + return klineData.map(item => ({ + timestamp: item.datetime / 1000000000, // 转换为秒 + open: item.open, + high: item.high, + low: item.low, + close: item.close, + volume: item.volume + })); + } catch (error) { + console.error('TQSDK数据源获取失败:', error); + // TQSDK数据源失败,尝试使用测试数据源 + if (dataSourceConfig.test?.enabled) { + console.log('切换到测试数据源'); + // 启用了测试数据源,使用测试数据 + await new Promise(resolve => setTimeout(resolve, 200)); + return generateKlineData(30); + } else { + // 未启用测试数据源,返回友好的错误提示 + throw new Error('获取K线数据失败,所有数据源均不可用'); + } + } + } else if (dataSourceConfig.test?.enabled) { + // 直接使用测试数据源 + console.log('使用测试数据源'); await new Promise(resolve => setTimeout(resolve, 200)); return generateKlineData(30); } else { - // 未启用测试数据源,返回友好的错误提示 - throw new Error('获取K线数据失败,请检查数据源配置'); + // 无可用数据源 + throw new Error('无可用数据源,请在管理配置中启用至少一个数据源'); } + } catch (error) { + console.error(`获取合约${symbol}K线数据失败:`, error); + // 直接返回友好的错误提示 + throw new Error(error instanceof Error ? error.message : '获取K线数据失败,请检查数据源配置'); } }; @@ -204,27 +302,70 @@ export const fetchMarketHotspots = async () => { try { // 获取数据源配置 const dataSourceConfig = getDataSourceConfig(); - // 使用TQSDK数据源 - const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig); - const overview = await dataSource.getMarketOverview(); - // 按涨跌幅排序,返回前10个 - return overview - .sort((a, b) => Math.abs(b.change_percent) - Math.abs(a.change_percent)) - .slice(0, 10) - .map(item => ({ - symbol: item.symbol, - name: item.name, - change: item.change_percent, - volume: item.volume - })); - } catch (error) { - console.error('获取市场热点失败:', error); - // 获取数据源配置 - const dataSourceConfig = getDataSourceConfig(); - // 检查是否启用了测试数据源 - if (dataSourceConfig.test && dataSourceConfig.test.enabled) { - // 启用了测试数据源,使用测试数据 + // 检查是否有可用的数据源 + const hasAvailableDataSource = dataSourceConfig.tqsdk?.enabled || dataSourceConfig.test?.enabled; + if (!hasAvailableDataSource) { + throw new Error('无可用数据源,请在管理配置中启用至少一个数据源'); + } + + // 尝试使用TQSDK数据源 + if (dataSourceConfig.tqsdk?.enabled) { + try { + const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig); + + // 使用用户指定的合约列表 + const hotspots = []; + for (const future of futuresList) { + try { + // 构建合约符号 + const symbol = `${future.exchange}.${future.code}${new Date().getFullYear().toString().slice(-2)}05`; + // 获取合约详情和实时行情 + const tick = await dataSource.getTickData(symbol); + + hotspots.push({ + symbol: future.code, + name: future.name, + change: tick.price_change / tick.pre_close * 100, + volume: tick.volume + }); + } catch (error) { + console.error(`获取合约${future.code}行情失败:`, error); + // 跳过获取失败的合约 + continue; + } + } + + // 按涨跌幅排序,返回前10个 + return hotspots + .sort((a, b) => Math.abs(b.change) - Math.abs(a.change)) + .slice(0, 10); + } catch (error) { + console.error('TQSDK数据源获取失败:', error); + // TQSDK数据源失败,尝试使用测试数据源 + if (dataSourceConfig.test?.enabled) { + console.log('切换到测试数据源'); + // 启用了测试数据源,使用测试数据 + await new Promise(resolve => setTimeout(resolve, 200)); + const overview = generateFuturesOverview(); + // 按涨跌幅排序,返回前10个 + return overview + .sort((a, b) => Math.abs(b.changePercent) - Math.abs(a.changePercent)) + .slice(0, 10) + .map(item => ({ + symbol: item.code, + name: item.name, + change: item.changePercent, + volume: Math.floor(Math.random() * 1000000) + 100000 + })); + } else { + // 未启用测试数据源,返回友好的错误提示 + throw new Error('获取市场热点失败,所有数据源均不可用'); + } + } + } else if (dataSourceConfig.test?.enabled) { + // 直接使用测试数据源 + console.log('使用测试数据源'); await new Promise(resolve => setTimeout(resolve, 200)); const overview = generateFuturesOverview(); // 按涨跌幅排序,返回前10个 @@ -238,9 +379,13 @@ export const fetchMarketHotspots = async () => { volume: Math.floor(Math.random() * 1000000) + 100000 })); } else { - // 未启用测试数据源,返回友好的错误提示 - throw new Error('获取市场热点失败,请检查数据源配置'); + // 无可用数据源 + throw new Error('无可用数据源,请在管理配置中启用至少一个数据源'); } + } catch (error) { + console.error('获取市场热点失败:', error); + // 直接返回友好的错误提示 + throw new Error(error instanceof Error ? error.message : '获取市场热点失败,请检查数据源配置'); } }; diff --git a/backend/src/utils/mockData.ts b/backend/src/utils/mockData.ts index 2be010c..91bc19d 100644 --- a/backend/src/utils/mockData.ts +++ b/backend/src/utils/mockData.ts @@ -3,44 +3,65 @@ // 期货品种列表 export const futuresList = [ // 金属类 - { code: 'AU', name: '金', type: '金属' }, - { code: 'AG', name: '银', type: '金属' }, - { code: 'CU', name: '铜', type: '金属' }, - { code: 'NI', name: '镍', type: '金属' }, - { code: 'SN', name: '锡', type: '金属' }, - { code: 'AL', name: '铝', type: '金属' }, - { code: 'ZN', name: '锌', type: '金属' }, + { code: 'AU', name: '黄金', type: '金属', exchange: 'SHFE' }, + { code: 'AG', name: '白银', type: '金属', exchange: 'SHFE' }, + { code: 'CU', name: '铜', type: '金属', exchange: 'SHFE' }, + { code: 'NI', name: '镍', type: '金属', exchange: 'SHFE' }, + { code: 'SN', name: '锡', type: '金属', exchange: 'SHFE' }, + { code: 'AL', name: '铝', type: '金属', exchange: 'SHFE' }, + { code: 'ZN', name: '锌', type: '金属', exchange: 'SHFE' }, + { code: 'PB', name: '铅', type: '金属', exchange: 'SHFE' }, // 建材类 - { code: 'FG', name: '玻璃', type: '建材' }, - { code: 'SJS', name: '烧碱', type: '建材' }, - { code: 'SCA', name: '纯碱', type: '建材' }, - { code: 'JM', name: '焦煤', type: '建材' }, - { code: 'RB', name: '螺纹钢', type: '建材' }, - { code: 'ALO', name: '氧化铝', type: '建材' }, + { code: 'FG', name: '玻璃', type: '建材', exchange: 'CZCE' }, + { code: 'LY', name: '烧碱', type: '建材', exchange: 'CZCE' }, + { code: 'SA', name: '纯碱', type: '建材', exchange: 'CZCE' }, + { code: 'JM', name: '焦煤', type: '建材', exchange: 'DCE' }, + { code: 'RB', name: '螺纹钢', type: '建材', exchange: 'SHFE' }, + { code: 'ALO', name: '氧化铝', type: '建材', exchange: 'SHFE' }, + { code: 'HC', name: '热轧卷板', type: '建材', exchange: 'SHFE' }, // 能源化工类 - { code: 'MA', name: '甲醇', type: '能源化工' }, - { code: 'PVC', name: 'PVC', type: '能源化工' }, - { code: 'FU', name: '燃油', type: '能源化工' }, - { code: 'SC', name: '原油', type: '能源化工' }, - { code: 'L', name: '橡胶', type: '能源化工' }, - { code: 'NR', name: '20号胶', type: '能源化工' }, - { code: 'BU', name: '沥青', type: '能源化工' }, - { code: 'LU', name: '低硫燃油', type: '能源化工' }, + { code: 'MA', name: '甲醇', type: '能源化工', exchange: 'CZCE' }, + { code: 'V', name: 'PVC', type: '能源化工', exchange: 'DCE' }, + { code: 'FU', name: '燃油', type: '能源化工', exchange: 'SHFE' }, + { code: 'SC', name: '原油', type: '能源化工', exchange: 'INE' }, + { code: 'RU', name: '橡胶', type: '能源化工', exchange: 'SHFE' }, + { code: 'BR', name: '合成橡胶', type: '能源化工', exchange: 'DCE' }, + { code: 'NR', name: '20号胶', type: '能源化工', exchange: 'SHFE' }, + { code: 'BU', name: '沥青', type: '能源化工', exchange: 'SHFE' }, + { code: 'LU', name: '低硫燃油', type: '能源化工', exchange: 'INE' }, + { code: 'L', name: '聚乙烯', type: '能源化工', exchange: 'DCE' }, + { code: 'PP', name: '聚丙烯', type: '能源化工', exchange: 'DCE' }, + { code: 'TA', name: 'PTA', type: '能源化工', exchange: 'CZCE' }, // 农产品类 - { code: 'P', name: '棕榈油', type: '农产品' }, + { code: 'P', name: '棕榈油', type: '农产品', exchange: 'DCE' }, + { code: 'A', name: '大豆', type: '农产品', exchange: 'DCE' }, + { code: 'B', name: '豆粕', type: '农产品', exchange: 'DCE' }, + { code: 'M', name: '豆粕', type: '农产品', exchange: 'DCE' }, + { code: 'Y', name: '豆油', type: '农产品', exchange: 'DCE' }, + { code: 'C', name: '玉米', type: '农产品', exchange: 'DCE' }, + { code: 'CS', name: '玉米淀粉', type: '农产品', exchange: 'DCE' }, + { code: 'CF', name: '棉花', type: '农产品', exchange: 'CZCE' }, + { code: 'SR', name: '白糖', type: '农产品', exchange: 'CZCE' }, + { code: 'RM', name: '菜籽粕', type: '农产品', exchange: 'CZCE' }, + { code: 'OI', name: '菜籽油', type: '农产品', exchange: 'CZCE' }, // 新能源类 - { code: 'LC', name: '碳酸锂', type: '新能源' }, - { code: 'SI', name: '工业硅', type: '新能源' }, - { code: 'PGS', name: '多晶硅', type: '新能源' }, + { code: 'LI', name: '碳酸锂', type: '新能源', exchange: 'SHFE' }, + { code: 'SI', name: '工业硅', type: '新能源', exchange: 'GEM' }, + { code: 'SP', name: '多晶硅', type: '新能源', exchange: 'GEM' }, // 金融类 - { code: 'IC', name: '中证500', type: '金融' }, - { code: 'IM', name: '中证1000', type: '金融' }, - { code: 'IH', name: '上证50', type: '金融' } + { code: 'IM', name: '中证1000', type: '金融', exchange: 'CFFEX' }, + { code: 'IC', name: '中证500', type: '金融', exchange: 'CFFEX' }, + { code: 'IH', name: '上证50', type: '金融', exchange: 'CFFEX' }, + + // 其他 + { code: 'I', name: '铁矿石', type: '其他', exchange: 'DCE' }, + { code: 'J', name: '焦炭', type: '其他', exchange: 'DCE' }, + { code: 'ZC', name: '动力煤', type: '其他', exchange: 'CZCE' } ]; // 生成随机数据的工具函数 diff --git a/src/App.jsx b/src/App.jsx index f1abf88..242955c 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,6 +1,7 @@ import React, { Suspense, lazy } from 'react'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { Provider } from 'react-redux'; +import { App as AntdApp } from 'antd'; import store from './store'; import MainLayout from './components/layout/MainLayout'; // 使用懒加载优化性能 @@ -14,22 +15,24 @@ import './App.css'; function App() { return ( - - - - 加载中...}> - - } /> - } /> - } /> - } /> - } /> - } /> - - - - - + + + + + 加载中...}> + + } /> + } /> + } /> + } /> + } /> + } /> + + + + + + ); } diff --git a/src/pages/admin/AdminConfig.jsx b/src/pages/admin/AdminConfig.jsx index 13eed80..3f8b61d 100644 --- a/src/pages/admin/AdminConfig.jsx +++ b/src/pages/admin/AdminConfig.jsx @@ -1,5 +1,6 @@ -import React, { useState } from 'react'; -import { Card, Row, Col, Form, Input, Button, Select, Switch, InputNumber, Alert, Divider, Tabs, message } from 'antd'; +import React, { useState, useEffect } from 'react'; +import { Card, Row, Col, Form, Input, Button, Select, Switch, InputNumber, Alert, Divider, Tabs } from 'antd'; +import { message } from 'antd'; import { DatabaseOutlined, KeyOutlined, SettingOutlined, SaveOutlined, ToolOutlined } from '@ant-design/icons'; import './AdminConfig.css'; @@ -9,6 +10,12 @@ const { TabPane } = Tabs; const AdminConfig = () => { const [form] = Form.useForm(); + const [messageApi, contextHolder] = message.useMessage(); + + // 组件挂载时获取配置 + useEffect(() => { + fetchConfig(); + }, []); // 数据库配置 const databaseConfig = { @@ -163,10 +170,44 @@ const AdminConfig = () => { })); }; + // 从后端获取当前配置 + const fetchConfig = async () => { + try { + const response = await fetch('http://localhost:3007/api/config/get'); + const result = await response.json(); + if (result.success) { + const newConfig = result.data; + setConfig(newConfig); + + // 更新表单字段值 + form.setFieldsValue({ + database: newConfig.database, + server: newConfig.server, + security: { + ...newConfig.security, + cors: { + ...newConfig.security.cors, + methods: newConfig.security.cors.methods.join(', '), + allowedHeaders: newConfig.security.cors.allowedHeaders.join(', ') + } + }, + dataSource: newConfig.dataSource + }); + + messageApi.success('配置加载成功'); + } else { + messageApi.error('配置加载失败'); + } + } catch (error) { + console.error('获取配置失败:', error); + messageApi.error('获取配置失败,请检查网络连接'); + } + }; + // 处理表单提交 const handleSubmit = async (values) => { try { - const response = await fetch('http://localhost:3005/api/config/save', { + const response = await fetch('http://localhost:3007/api/config/save', { method: 'POST', headers: { 'Content-Type': 'application/json' @@ -176,20 +217,22 @@ const AdminConfig = () => { const result = await response.json(); if (result.success) { - message.success(result.message); + messageApi.success(result.message); + // 保存成功后重新获取配置 + fetchConfig(); } else { - message.error(result.message); + messageApi.error(result.message); } } catch (error) { console.error('保存配置失败:', error); - message.error('保存配置失败,请检查网络连接'); + messageApi.error('保存配置失败,请检查网络连接'); } }; // 测试数据库连接 const testDatabaseConnection = async (dbType) => { try { - const response = await fetch('http://localhost:3005/api/config/test-database', { + const response = await fetch('http://localhost:3007/api/config/test-database', { method: 'POST', headers: { 'Content-Type': 'application/json' @@ -202,20 +245,20 @@ const AdminConfig = () => { const result = await response.json(); if (result.success) { - message.success(result.message); + messageApi.success(result.message); } else { - message.error(result.message); + messageApi.error(result.message); } } catch (error) { console.error('测试数据库连接失败:', error); - message.error('测试数据库连接失败,请检查网络连接'); + messageApi.error('测试数据库连接失败,请检查网络连接'); } }; // 测试数据源连接 const testDataSourceConnection = async (dsType) => { try { - const response = await fetch('http://localhost:3005/api/config/test-datasource', { + const response = await fetch('http://localhost:3007/api/config/test-datasource', { method: 'POST', headers: { 'Content-Type': 'application/json' @@ -228,18 +271,20 @@ const AdminConfig = () => { const result = await response.json(); if (result.success) { - message.success(result.message); + messageApi.success(result.message); } else { - message.error(result.message); + messageApi.error(result.message); } } catch (error) { console.error('测试数据源连接失败:', error); - message.error('测试数据源连接失败,请检查网络连接'); + messageApi.error('测试数据源连接失败,请检查网络连接'); } }; return ( -
+ <> + {contextHolder} +

管理配置

{
-
+ + ); }; diff --git a/src/pages/dashboard/Dashboard.jsx b/src/pages/dashboard/Dashboard.jsx index 67ae8fc..0016746 100644 --- a/src/pages/dashboard/Dashboard.jsx +++ b/src/pages/dashboard/Dashboard.jsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { Card, Row, Col, Statistic, Button, Select, Tag, message, Spin, Alert, Modal, Form, InputNumber, Switch, Checkbox } from 'antd'; +import { Card, Row, Col, Statistic, Button, Select, Tag, Spin, Alert, Modal, Form, InputNumber, Switch, Checkbox } from 'antd'; +import { message } from 'antd'; import { ReloadOutlined, ArrowUpOutlined, ArrowDownOutlined, FireOutlined, AlertOutlined, RobotOutlined, BellOutlined } from '@ant-design/icons'; import { fetchFuturesOverview, fetchRiskAlerts, fetchAIMarketAnalysis, toggleWatchlist } from '../../store/futuresSlice'; import { useNavigate } from 'react-router-dom'; @@ -18,6 +19,7 @@ const Dashboard = () => { const [pushModalVisible, setPushModalVisible] = useState(false); const [currentFuture, setCurrentFuture] = useState(null); const [pushForm] = Form.useForm(); + const [messageApi, contextHolder] = message.useMessage(); useEffect(() => { dispatch(fetchFuturesOverview()); @@ -29,7 +31,7 @@ const Dashboard = () => { dispatch(fetchFuturesOverview()); dispatch(fetchRiskAlerts()); dispatch(fetchAIMarketAnalysis()); - message.success('数据已刷新'); + messageApi.success('数据已刷新'); }; const handleFutureClick = (future) => { @@ -39,7 +41,7 @@ const Dashboard = () => { const handleToggleWatchlist = (future, e) => { e.stopPropagation(); dispatch(toggleWatchlist(future.code)); - message.success(future.isInWatchlist ? '已从自选移除' : '已添加到自选'); + messageApi.success(future.isInWatchlist ? '已从自选移除' : '已添加到自选'); }; const openPushConfig = (future, e) => { @@ -52,7 +54,7 @@ const Dashboard = () => { console.log('消息推送配置保存:', values); // 模拟保存操作 setPushModalVisible(false); - message.success('消息推送配置已保存'); + messageApi.success('消息推送配置已保存'); }; const getChangeColor = (changePercent) => { @@ -110,7 +112,9 @@ const Dashboard = () => { } return ( -
+ <> + {contextHolder} +
{/* 页面头部 */}

市场概览

@@ -469,7 +473,8 @@ const Dashboard = () => { -
+
+ ); }; diff --git a/src/pages/watchlist/Watchlist.jsx b/src/pages/watchlist/Watchlist.jsx index bda1797..ee5868e 100644 --- a/src/pages/watchlist/Watchlist.jsx +++ b/src/pages/watchlist/Watchlist.jsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { Card, Row, Col, Button, Select, Tag, message, Spin, Modal, Form, InputNumber, Switch } from 'antd'; +import { Card, Row, Col, Button, Select, Tag, Spin, Modal, Form, InputNumber, Switch } from 'antd'; +import { message } from 'antd'; import { ReloadOutlined, ArrowUpOutlined, ArrowDownOutlined, StarOutlined, DeleteOutlined, BellOutlined } from '@ant-design/icons'; import { fetchFuturesOverview, toggleWatchlist } from '../../store/futuresSlice'; import { useNavigate } from 'react-router-dom'; @@ -17,6 +18,7 @@ const Watchlist = () => { const [pushModalVisible, setPushModalVisible] = useState(false); const [currentFuture, setCurrentFuture] = useState(null); const [pushForm] = Form.useForm(); + const [messageApi, contextHolder] = message.useMessage(); useEffect(() => { dispatch(fetchFuturesOverview()); @@ -24,7 +26,7 @@ const Watchlist = () => { const handleRefresh = () => { dispatch(fetchFuturesOverview()); - message.success('数据已刷新'); + messageApi.success('数据已刷新'); }; const handleFutureClick = (future) => { @@ -33,7 +35,7 @@ const Watchlist = () => { const handleToggleWatchlist = (future) => { dispatch(toggleWatchlist(future.code)); - message.success(future.isInWatchlist ? '已从自选移除' : '已添加到自选'); + messageApi.success(future.isInWatchlist ? '已从自选移除' : '已添加到自选'); }; const openPushConfig = (future, e) => { @@ -46,7 +48,7 @@ const Watchlist = () => { console.log('消息推送配置保存:', values); // 模拟保存操作 setPushModalVisible(false); - message.success('消息推送配置已保存'); + messageApi.success('消息推送配置已保存'); }; const getChangeColor = (changePercent) => { @@ -78,7 +80,9 @@ const Watchlist = () => { const watchlistData = getWatchlistData(); return ( -
+ <> + {contextHolder} +
{/* 页面头部 */}

自选合约

@@ -351,7 +355,8 @@ const Watchlist = () => { -
+
+ ); }; diff --git a/src/utils/mockData.js b/src/utils/mockData.js index 191ff51..5c3267f 100644 --- a/src/utils/mockData.js +++ b/src/utils/mockData.js @@ -3,44 +3,65 @@ // 期货品种列表 export const futuresList = [ // 金属类 - { code: 'AU', name: '金', type: '金属' }, - { code: 'AG', name: '银', type: '金属' }, - { code: 'CU', name: '铜', type: '金属' }, - { code: 'NI', name: '镍', type: '金属' }, - { code: 'SN', name: '锡', type: '金属' }, - { code: 'AL', name: '铝', type: '金属' }, - { code: 'ZN', name: '锌', type: '金属' }, + { code: 'AU', name: '黄金', type: '金属', exchange: 'SHFE' }, + { code: 'AG', name: '白银', type: '金属', exchange: 'SHFE' }, + { code: 'CU', name: '铜', type: '金属', exchange: 'SHFE' }, + { code: 'NI', name: '镍', type: '金属', exchange: 'SHFE' }, + { code: 'SN', name: '锡', type: '金属', exchange: 'SHFE' }, + { code: 'AL', name: '铝', type: '金属', exchange: 'SHFE' }, + { code: 'ZN', name: '锌', type: '金属', exchange: 'SHFE' }, + { code: 'PB', name: '铅', type: '金属', exchange: 'SHFE' }, // 建材类 - { code: 'FG', name: '玻璃', type: '建材' }, - { code: 'SJS', name: '烧碱', type: '建材' }, - { code: 'SCA', name: '纯碱', type: '建材' }, - { code: 'JM', name: '焦煤', type: '建材' }, - { code: 'RB', name: '螺纹钢', type: '建材' }, - { code: 'ALO', name: '氧化铝', type: '建材' }, + { code: 'FG', name: '玻璃', type: '建材', exchange: 'CZCE' }, + { code: 'LY', name: '烧碱', type: '建材', exchange: 'CZCE' }, + { code: 'SA', name: '纯碱', type: '建材', exchange: 'CZCE' }, + { code: 'JM', name: '焦煤', type: '建材', exchange: 'DCE' }, + { code: 'RB', name: '螺纹钢', type: '建材', exchange: 'SHFE' }, + { code: 'ALO', name: '氧化铝', type: '建材', exchange: 'SHFE' }, + { code: 'HC', name: '热轧卷板', type: '建材', exchange: 'SHFE' }, // 能源化工类 - { code: 'MA', name: '甲醇', type: '能源化工' }, - { code: 'PVC', name: 'PVC', type: '能源化工' }, - { code: 'FU', name: '燃油', type: '能源化工' }, - { code: 'SC', name: '原油', type: '能源化工' }, - { code: 'L', name: '橡胶', type: '能源化工' }, - { code: 'NR', name: '20号胶', type: '能源化工' }, - { code: 'BU', name: '沥青', type: '能源化工' }, - { code: 'LU', name: '低硫燃油', type: '能源化工' }, + { code: 'MA', name: '甲醇', type: '能源化工', exchange: 'CZCE' }, + { code: 'V', name: 'PVC', type: '能源化工', exchange: 'DCE' }, + { code: 'FU', name: '燃油', type: '能源化工', exchange: 'SHFE' }, + { code: 'SC', name: '原油', type: '能源化工', exchange: 'INE' }, + { code: 'RU', name: '橡胶', type: '能源化工', exchange: 'SHFE' }, + { code: 'BR', name: '合成橡胶', type: '能源化工', exchange: 'DCE' }, + { code: 'NR', name: '20号胶', type: '能源化工', exchange: 'SHFE' }, + { code: 'BU', name: '沥青', type: '能源化工', exchange: 'SHFE' }, + { code: 'LU', name: '低硫燃油', type: '能源化工', exchange: 'INE' }, + { code: 'L', name: '聚乙烯', type: '能源化工', exchange: 'DCE' }, + { code: 'PP', name: '聚丙烯', type: '能源化工', exchange: 'DCE' }, + { code: 'TA', name: 'PTA', type: '能源化工', exchange: 'CZCE' }, // 农产品类 - { code: 'P', name: '棕榈油', type: '农产品' }, + { code: 'P', name: '棕榈油', type: '农产品', exchange: 'DCE' }, + { code: 'A', name: '大豆', type: '农产品', exchange: 'DCE' }, + { code: 'B', name: '豆粕', type: '农产品', exchange: 'DCE' }, + { code: 'M', name: '豆粕', type: '农产品', exchange: 'DCE' }, + { code: 'Y', name: '豆油', type: '农产品', exchange: 'DCE' }, + { code: 'C', name: '玉米', type: '农产品', exchange: 'DCE' }, + { code: 'CS', name: '玉米淀粉', type: '农产品', exchange: 'DCE' }, + { code: 'CF', name: '棉花', type: '农产品', exchange: 'CZCE' }, + { code: 'SR', name: '白糖', type: '农产品', exchange: 'CZCE' }, + { code: 'RM', name: '菜籽粕', type: '农产品', exchange: 'CZCE' }, + { code: 'OI', name: '菜籽油', type: '农产品', exchange: 'CZCE' }, // 新能源类 - { code: 'LC', name: '碳酸锂', type: '新能源' }, - { code: 'SI', name: '工业硅', type: '新能源' }, - { code: 'PGS', name: '多晶硅', type: '新能源' }, + { code: 'LI', name: '碳酸锂', type: '新能源', exchange: 'SHFE' }, + { code: 'SI', name: '工业硅', type: '新能源', exchange: 'GEM' }, + { code: 'SP', name: '多晶硅', type: '新能源', exchange: 'GEM' }, // 金融类 - { code: 'IC', name: '中证500', type: '金融' }, - { code: 'IM', name: '中证1000', type: '金融' }, - { code: 'IH', name: '上证50', type: '金融' } + { code: 'IM', name: '中证1000', type: '金融', exchange: 'CFFEX' }, + { code: 'IC', name: '中证500', type: '金融', exchange: 'CFFEX' }, + { code: 'IH', name: '上证50', type: '金融', exchange: 'CFFEX' }, + + // 其他 + { code: 'I', name: '铁矿石', type: '其他', exchange: 'DCE' }, + { code: 'J', name: '焦炭', type: '其他', exchange: 'DCE' }, + { code: 'ZC', name: '动力煤', type: '其他', exchange: 'CZCE' } ]; // 生成随机数据的工具函数