fix: 增加启动文档,尝试解决配置问题bug

master
Lxy 3 months ago
parent 8bcd039e9d
commit fc402d80d9

@ -14,188 +14,286 @@ export const fetchMarketOverview = async () => {
try { try {
// 获取数据源配置 // 获取数据源配置
const dataSourceConfig = getDataSourceConfig(); const dataSourceConfig = getDataSourceConfig();
// 使用TQSDK数据源
const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig); // 检查是否有可用的数据源
const overview = await dataSource.getMarketOverview(); const hasAvailableDataSource = dataSourceConfig.tqsdk?.enabled || dataSourceConfig.test?.enabled;
if (!hasAvailableDataSource) {
// 转换为前端需要的格式 throw new Error('无可用数据源,请在管理配置中启用至少一个数据源');
return overview.map(item => ({ }
code: item.symbol,
name: item.name, // 尝试使用TQSDK数据源
currentPrice: item.price, if (dataSourceConfig.tqsdk?.enabled) {
changePercent: item.change_percent, try {
winRate: Math.floor(Math.random() * 50) + 30, // 模拟胜率 const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig);
atr: +(Math.random() * 5 + 0.5).toFixed(2), // 模拟ATR
adx: Math.floor(Math.random() * 60) + 10, // 模拟ADX // 使用用户指定的合约列表
adxStatus: adx => { const overview = [];
if (adx < 20) return '无趋势/震荡'; for (const future of futuresList) {
if (adx < 40) return '弱趋势'; try {
return '强趋势'; // 构建合约符号
}, const symbol = `${future.exchange}.${future.code}${new Date().getFullYear().toString().slice(-2)}05`;
trends: { // 获取合约详情和实时行情
'5MIN': { const tick = await dataSource.getTickData(symbol);
direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)],
status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], overview.push({
rsi: Math.floor(Math.random() * 80) + 10 code: future.code,
}, name: future.name,
'30MIN': { currentPrice: tick.last_price,
direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], changePercent: tick.price_change / tick.pre_close * 100,
status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], winRate: Math.floor(Math.random() * 50) + 30, // 模拟胜率
rsi: Math.floor(Math.random() * 80) + 10 atr: +(Math.random() * 5 + 0.5).toFixed(2), // 模拟ATR
}, adx: Math.floor(Math.random() * 60) + 10, // 模拟ADX
'1HOUR': { adxStatus: adx => {
direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], if (adx < 20) return '无趋势/震荡';
status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], if (adx < 40) return '弱趋势';
rsi: Math.floor(Math.random() * 80) + 10 return '强趋势';
}, },
'1DAY': { trends: {
direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], '5MIN': {
status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)],
rsi: Math.floor(Math.random() * 80) + 10 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: { return overview;
entry: item.price, } catch (error) {
stopLoss: item.price * (1 - 0.02 * (Math.random() + 0.5)), console.error('TQSDK数据源获取失败:', error);
target: item.price * (1 + 0.03 * (Math.random() + 0.5)), // TQSDK数据源失败尝试使用测试数据源
resistance: item.price * (1 + 0.05 * (Math.random() + 0.5)), if (dataSourceConfig.test?.enabled) {
support: item.price * (1 - 0.05 * (Math.random() + 0.5)) console.log('切换到测试数据源');
}, // 启用了测试数据源,使用测试数据
overallView: ['观望', '中线', '多头排列', '空头排列', '震荡'][Math.floor(Math.random() * 5)], await new Promise(resolve => setTimeout(resolve, 300));
aiAnalysis: `MACD:金叉向上 | RSI:${Math.floor(Math.random() * 80) + 10}(中性) | 布林带:中轨附近` return generateFuturesOverview();
})); } else {
} catch (error) { // 未启用测试数据源,返回友好的错误提示
console.error('获取市场概览失败:', error); throw new Error('获取市场概览失败,所有数据源均不可用');
// 获取数据源配置 }
const dataSourceConfig = getDataSourceConfig(); }
// 检查是否启用了测试数据源 } else if (dataSourceConfig.test?.enabled) {
if (dataSourceConfig.test && dataSourceConfig.test.enabled) { // 直接使用测试数据源
// 启用了测试数据源,使用测试数据 console.log('使用测试数据源');
await new Promise(resolve => setTimeout(resolve, 300)); await new Promise(resolve => setTimeout(resolve, 300));
return generateFuturesOverview(); return generateFuturesOverview();
} else { } 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) => { export const fetchMarketDetail = async (symbol: string) => {
try { try {
// 查找合约信息
const future = futuresList.find(item => item.code === symbol);
if (!future) {
throw new Error('品种不存在');
}
// 获取数据源配置 // 获取数据源配置
const dataSourceConfig = getDataSourceConfig(); const dataSourceConfig = getDataSourceConfig();
// 使用TQSDK数据源
const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig); // 检查是否有可用的数据源
const contract = await dataSource.getContractDetail(symbol); const hasAvailableDataSource = dataSourceConfig.tqsdk?.enabled || dataSourceConfig.test?.enabled;
const tick = await dataSource.getTickData(symbol); if (!hasAvailableDataSource) {
throw new Error('无可用数据源,请在管理配置中启用至少一个数据源');
// 转换为前端需要的格式 }
return {
code: contract.symbol, // 尝试使用TQSDK数据源
name: contract.name, if (dataSourceConfig.tqsdk?.enabled) {
fullName: `${contract.name}-${contract.symbol}605`, try {
currentPrice: tick.last_price, const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig);
changePercent: tick.price_change / tick.pre_close * 100,
winRate: Math.floor(Math.random() * 50) + 30, // 构建合约符号
atr: +(Math.random() * 5 + 0.5).toFixed(2), const contractSymbol = `${future.exchange}.${future.code}${new Date().getFullYear().toString().slice(-2)}05`;
adx: Math.floor(Math.random() * 60) + 10,
adxStatus: adx => { // 获取合约详情和实时行情
if (adx < 20) return '无趋势/震荡'; const tick = await dataSource.getTickData(contractSymbol);
if (adx < 40) return '弱趋势';
return '强趋势'; // 转换为前端需要的格式
}, return {
trends: { code: future.code,
'5MIN': { name: future.name,
direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], fullName: `${future.name}-${future.code}605`,
status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], currentPrice: tick.last_price,
rsi: Math.floor(Math.random() * 80) + 10 changePercent: tick.price_change / tick.pre_close * 100,
}, winRate: Math.floor(Math.random() * 50) + 30,
'30MIN': { atr: +(Math.random() * 5 + 0.5).toFixed(2),
direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], adx: Math.floor(Math.random() * 60) + 10,
status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], adxStatus: adx => {
rsi: Math.floor(Math.random() * 80) + 10 if (adx < 20) return '无趋势/震荡';
}, if (adx < 40) return '弱趋势';
'1HOUR': { return '强趋势';
direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], },
status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)], trends: {
rsi: Math.floor(Math.random() * 80) + 10 '5MIN': {
}, direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)],
'1DAY': { status: ['多头趋势', '空头趋势', '震荡'][Math.floor(Math.random() * 3)],
direction: ['看多', '看空', '观望'][Math.floor(Math.random() * 3)], rsi: Math.floor(Math.random() * 80) + 10
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); return generateFutureData(symbol, future.name);
} else { } else {
// 未启用测试数据源,返回友好的错误提示 // 无可用数据源
throw new Error('获取品种详情失败,请检查数据源配置'); throw new Error('无可用数据源,请在管理配置中启用至少一个数据源');
} }
} catch (error) {
console.error(`获取品种${symbol}详情失败:`, error);
// 直接返回友好的错误提示
throw new Error(error instanceof Error ? error.message : '获取品种详情失败,请检查数据源配置');
} }
}; };
// 获取K线数据 // 获取K线数据
export const fetchKlineData = async (symbol: string, period: string) => { export const fetchKlineData = async (symbol: string, period: string) => {
try { try {
// 查找合约信息
const future = futuresList.find(item => item.code === symbol);
if (!future) {
throw new Error('品种不存在');
}
// 获取数据源配置 // 获取数据源配置
const dataSourceConfig = getDataSourceConfig(); const dataSourceConfig = getDataSourceConfig();
// 使用TQSDK数据源
const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig); // 检查是否有可用的数据源
const klineData = await dataSource.getKlineData(symbol, period, 30); const hasAvailableDataSource = dataSourceConfig.tqsdk?.enabled || dataSourceConfig.test?.enabled;
if (!hasAvailableDataSource) {
// 转换为前端需要的格式 throw new Error('无可用数据源,请在管理配置中启用至少一个数据源');
return klineData.map(item => ({ }
timestamp: item.datetime / 1000000000, // 转换为秒
open: item.open, // 尝试使用TQSDK数据源
high: item.high, if (dataSourceConfig.tqsdk?.enabled) {
low: item.low, try {
close: item.close, const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig);
volume: item.volume
})); // 构建合约符号
} catch (error) { const contractSymbol = `${future.exchange}.${future.code}${new Date().getFullYear().toString().slice(-2)}05`;
console.error(`获取合约${symbol}K线数据失败:`, error);
// 获取数据源配置 // 获取K线数据
const dataSourceConfig = getDataSourceConfig(); const klineData = await dataSource.getKlineData(contractSymbol, period, 30);
// 检查是否启用了测试数据源
if (dataSourceConfig.test && dataSourceConfig.test.enabled) { // 转换为前端需要的格式
// 启用了测试数据源,使用测试数据 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)); await new Promise(resolve => setTimeout(resolve, 200));
return generateKlineData(30); return generateKlineData(30);
} else { } 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 { try {
// 获取数据源配置 // 获取数据源配置
const dataSourceConfig = getDataSourceConfig(); const dataSourceConfig = getDataSourceConfig();
// 使用TQSDK数据源
const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig); // 检查是否有可用的数据源
const overview = await dataSource.getMarketOverview(); const hasAvailableDataSource = dataSourceConfig.tqsdk?.enabled || dataSourceConfig.test?.enabled;
if (!hasAvailableDataSource) {
// 按涨跌幅排序返回前10个 throw new Error('无可用数据源,请在管理配置中启用至少一个数据源');
return overview }
.sort((a, b) => Math.abs(b.change_percent) - Math.abs(a.change_percent))
.slice(0, 10) // 尝试使用TQSDK数据源
.map(item => ({ if (dataSourceConfig.tqsdk?.enabled) {
symbol: item.symbol, try {
name: item.name, const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig);
change: item.change_percent,
volume: item.volume // 使用用户指定的合约列表
})); const hotspots = [];
} catch (error) { for (const future of futuresList) {
console.error('获取市场热点失败:', error); try {
// 获取数据源配置 // 构建合约符号
const dataSourceConfig = getDataSourceConfig(); const symbol = `${future.exchange}.${future.code}${new Date().getFullYear().toString().slice(-2)}05`;
// 检查是否启用了测试数据源 // 获取合约详情和实时行情
if (dataSourceConfig.test && dataSourceConfig.test.enabled) { 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)); await new Promise(resolve => setTimeout(resolve, 200));
const overview = generateFuturesOverview(); const overview = generateFuturesOverview();
// 按涨跌幅排序返回前10个 // 按涨跌幅排序返回前10个
@ -238,9 +379,13 @@ export const fetchMarketHotspots = async () => {
volume: Math.floor(Math.random() * 1000000) + 100000 volume: Math.floor(Math.random() * 1000000) + 100000
})); }));
} else { } else {
// 未启用测试数据源,返回友好的错误提示 // 无可用数据源
throw new Error('获取市场热点失败,请检查数据源配置'); throw new Error('无可用数据源,请在管理配置中启用至少一个数据源');
} }
} catch (error) {
console.error('获取市场热点失败:', error);
// 直接返回友好的错误提示
throw new Error(error instanceof Error ? error.message : '获取市场热点失败,请检查数据源配置');
} }
}; };

@ -3,44 +3,65 @@
// 期货品种列表 // 期货品种列表
export const futuresList = [ export const futuresList = [
// 金属类 // 金属类
{ code: 'AU', name: '金', type: '金属' }, { code: 'AU', name: '黄金', type: '金属', exchange: 'SHFE' },
{ code: 'AG', name: '银', type: '金属' }, { code: 'AG', name: '白银', type: '金属', exchange: 'SHFE' },
{ code: 'CU', name: '铜', type: '金属' }, { code: 'CU', name: '铜', type: '金属', exchange: 'SHFE' },
{ code: 'NI', name: '镍', type: '金属' }, { code: 'NI', name: '镍', type: '金属', exchange: 'SHFE' },
{ code: 'SN', name: '锡', type: '金属' }, { code: 'SN', name: '锡', type: '金属', exchange: 'SHFE' },
{ code: 'AL', name: '铝', type: '金属' }, { code: 'AL', name: '铝', type: '金属', exchange: 'SHFE' },
{ code: 'ZN', name: '锌', type: '金属' }, { code: 'ZN', name: '锌', type: '金属', exchange: 'SHFE' },
{ code: 'PB', name: '铅', type: '金属', exchange: 'SHFE' },
// 建材类 // 建材类
{ code: 'FG', name: '玻璃', type: '建材' }, { code: 'FG', name: '玻璃', type: '建材', exchange: 'CZCE' },
{ code: 'SJS', name: '烧碱', type: '建材' }, { code: 'LY', name: '烧碱', type: '建材', exchange: 'CZCE' },
{ code: 'SCA', name: '纯碱', type: '建材' }, { code: 'SA', name: '纯碱', type: '建材', exchange: 'CZCE' },
{ code: 'JM', name: '焦煤', type: '建材' }, { code: 'JM', name: '焦煤', type: '建材', exchange: 'DCE' },
{ code: 'RB', name: '螺纹钢', type: '建材' }, { code: 'RB', name: '螺纹钢', type: '建材', exchange: 'SHFE' },
{ code: 'ALO', name: '氧化铝', type: '建材' }, { code: 'ALO', name: '氧化铝', type: '建材', exchange: 'SHFE' },
{ code: 'HC', name: '热轧卷板', type: '建材', exchange: 'SHFE' },
// 能源化工类 // 能源化工类
{ code: 'MA', name: '甲醇', type: '能源化工' }, { code: 'MA', name: '甲醇', type: '能源化工', exchange: 'CZCE' },
{ code: 'PVC', name: 'PVC', type: '能源化工' }, { code: 'V', name: 'PVC', type: '能源化工', exchange: 'DCE' },
{ code: 'FU', name: '燃油', type: '能源化工' }, { code: 'FU', name: '燃油', type: '能源化工', exchange: 'SHFE' },
{ code: 'SC', name: '原油', type: '能源化工' }, { code: 'SC', name: '原油', type: '能源化工', exchange: 'INE' },
{ code: 'L', name: '橡胶', type: '能源化工' }, { code: 'RU', name: '橡胶', type: '能源化工', exchange: 'SHFE' },
{ code: 'NR', name: '20号胶', type: '能源化工' }, { code: 'BR', name: '合成橡胶', type: '能源化工', exchange: 'DCE' },
{ code: 'BU', name: '沥青', type: '能源化工' }, { code: 'NR', name: '20号胶', type: '能源化工', exchange: 'SHFE' },
{ code: 'LU', name: '低硫燃油', type: '能源化工' }, { 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: 'LI', name: '碳酸锂', type: '新能源', exchange: 'SHFE' },
{ code: 'SI', name: '工业硅', type: '新能源' }, { code: 'SI', name: '工业硅', type: '新能源', exchange: 'GEM' },
{ code: 'PGS', name: '多晶硅', type: '新能源' }, { code: 'SP', name: '多晶硅', type: '新能源', exchange: 'GEM' },
// 金融类 // 金融类
{ code: 'IC', name: '中证500', type: '金融' }, { code: 'IM', name: '中证1000', type: '金融', exchange: 'CFFEX' },
{ code: 'IM', name: '中证1000', type: '金融' }, { code: 'IC', name: '中证500', type: '金融', exchange: 'CFFEX' },
{ code: 'IH', name: '上证50', type: '金融' } { 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' }
]; ];
// 生成随机数据的工具函数 // 生成随机数据的工具函数

@ -1,6 +1,7 @@
import React, { Suspense, lazy } from 'react'; import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { App as AntdApp } from 'antd';
import store from './store'; import store from './store';
import MainLayout from './components/layout/MainLayout'; import MainLayout from './components/layout/MainLayout';
// 使 // 使
@ -14,22 +15,24 @@ import './App.css';
function App() { function App() {
return ( return (
<Provider store={store}> <AntdApp>
<Router> <Provider store={store}>
<MainLayout> <Router>
<Suspense fallback={<div className="loading-container">加载中...</div>}> <MainLayout>
<Routes> <Suspense fallback={<div className="loading-container">加载中...</div>}>
<Route path="/" element={<Dashboard />} /> <Routes>
<Route path="/watchlist" element={<Watchlist />} /> <Route path="/" element={<Dashboard />} />
<Route path="/detail/:code" element={<Detail />} /> <Route path="/watchlist" element={<Watchlist />} />
<Route path="/risk-control" element={<RiskControl />} /> <Route path="/detail/:code" element={<Detail />} />
<Route path="/config" element={<Config />} /> <Route path="/risk-control" element={<RiskControl />} />
<Route path="/admin" element={<AdminConfig />} /> <Route path="/config" element={<Config />} />
</Routes> <Route path="/admin" element={<AdminConfig />} />
</Suspense> </Routes>
</MainLayout> </Suspense>
</Router> </MainLayout>
</Provider> </Router>
</Provider>
</AntdApp>
); );
} }

@ -1,5 +1,6 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { Card, Row, Col, Form, Input, Button, Select, Switch, InputNumber, Alert, Divider, Tabs, message } from 'antd'; 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 { DatabaseOutlined, KeyOutlined, SettingOutlined, SaveOutlined, ToolOutlined } from '@ant-design/icons';
import './AdminConfig.css'; import './AdminConfig.css';
@ -9,6 +10,12 @@ const { TabPane } = Tabs;
const AdminConfig = () => { const AdminConfig = () => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [messageApi, contextHolder] = message.useMessage();
//
useEffect(() => {
fetchConfig();
}, []);
// //
const databaseConfig = { 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) => { const handleSubmit = async (values) => {
try { try {
const response = await fetch('http://localhost:3005/api/config/save', { const response = await fetch('http://localhost:3007/api/config/save', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@ -176,20 +217,22 @@ const AdminConfig = () => {
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
message.success(result.message); messageApi.success(result.message);
//
fetchConfig();
} else { } else {
message.error(result.message); messageApi.error(result.message);
} }
} catch (error) { } catch (error) {
console.error('保存配置失败:', error); console.error('保存配置失败:', error);
message.error('保存配置失败,请检查网络连接'); messageApi.error('保存配置失败,请检查网络连接');
} }
}; };
// //
const testDatabaseConnection = async (dbType) => { const testDatabaseConnection = async (dbType) => {
try { 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', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@ -202,20 +245,20 @@ const AdminConfig = () => {
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
message.success(result.message); messageApi.success(result.message);
} else { } else {
message.error(result.message); messageApi.error(result.message);
} }
} catch (error) { } catch (error) {
console.error('测试数据库连接失败:', error); console.error('测试数据库连接失败:', error);
message.error('测试数据库连接失败,请检查网络连接'); messageApi.error('测试数据库连接失败,请检查网络连接');
} }
}; };
// //
const testDataSourceConnection = async (dsType) => { const testDataSourceConnection = async (dsType) => {
try { 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', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@ -228,18 +271,20 @@ const AdminConfig = () => {
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
message.success(result.message); messageApi.success(result.message);
} else { } else {
message.error(result.message); messageApi.error(result.message);
} }
} catch (error) { } catch (error) {
console.error('测试数据源连接失败:', error); console.error('测试数据源连接失败:', error);
message.error('测试数据源连接失败,请检查网络连接'); messageApi.error('测试数据源连接失败,请检查网络连接');
} }
}; };
return ( return (
<div className="admin-config"> <>
{contextHolder}
<div className="admin-config">
<h2>管理配置</h2> <h2>管理配置</h2>
<Alert <Alert
title="警告:此页面包含敏感配置信息,请谨慎操作" title="警告:此页面包含敏感配置信息,请谨慎操作"
@ -1183,7 +1228,8 @@ const AdminConfig = () => {
</Button> </Button>
</div> </div>
</Form> </Form>
</div> </div>
</>
); );
}; };

@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'; 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 { ReloadOutlined, ArrowUpOutlined, ArrowDownOutlined, FireOutlined, AlertOutlined, RobotOutlined, BellOutlined } from '@ant-design/icons';
import { fetchFuturesOverview, fetchRiskAlerts, fetchAIMarketAnalysis, toggleWatchlist } from '../../store/futuresSlice'; import { fetchFuturesOverview, fetchRiskAlerts, fetchAIMarketAnalysis, toggleWatchlist } from '../../store/futuresSlice';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@ -18,6 +19,7 @@ const Dashboard = () => {
const [pushModalVisible, setPushModalVisible] = useState(false); const [pushModalVisible, setPushModalVisible] = useState(false);
const [currentFuture, setCurrentFuture] = useState(null); const [currentFuture, setCurrentFuture] = useState(null);
const [pushForm] = Form.useForm(); const [pushForm] = Form.useForm();
const [messageApi, contextHolder] = message.useMessage();
useEffect(() => { useEffect(() => {
dispatch(fetchFuturesOverview()); dispatch(fetchFuturesOverview());
@ -29,7 +31,7 @@ const Dashboard = () => {
dispatch(fetchFuturesOverview()); dispatch(fetchFuturesOverview());
dispatch(fetchRiskAlerts()); dispatch(fetchRiskAlerts());
dispatch(fetchAIMarketAnalysis()); dispatch(fetchAIMarketAnalysis());
message.success('数据已刷新'); messageApi.success('数据已刷新');
}; };
const handleFutureClick = (future) => { const handleFutureClick = (future) => {
@ -39,7 +41,7 @@ const Dashboard = () => {
const handleToggleWatchlist = (future, e) => { const handleToggleWatchlist = (future, e) => {
e.stopPropagation(); e.stopPropagation();
dispatch(toggleWatchlist(future.code)); dispatch(toggleWatchlist(future.code));
message.success(future.isInWatchlist ? '已从自选移除' : '已添加到自选'); messageApi.success(future.isInWatchlist ? '已从自选移除' : '已添加到自选');
}; };
const openPushConfig = (future, e) => { const openPushConfig = (future, e) => {
@ -52,7 +54,7 @@ const Dashboard = () => {
console.log('消息推送配置保存:', values); console.log('消息推送配置保存:', values);
// //
setPushModalVisible(false); setPushModalVisible(false);
message.success('消息推送配置已保存'); messageApi.success('消息推送配置已保存');
}; };
const getChangeColor = (changePercent) => { const getChangeColor = (changePercent) => {
@ -110,7 +112,9 @@ const Dashboard = () => {
} }
return ( return (
<div className="dashboard"> <>
{contextHolder}
<div className="dashboard">
{/* 页面头部 */} {/* 页面头部 */}
<div className="dashboard-header"> <div className="dashboard-header">
<h2>市场概览</h2> <h2>市场概览</h2>
@ -469,7 +473,8 @@ const Dashboard = () => {
</Card> </Card>
</Form> </Form>
</Modal> </Modal>
</div> </div>
</>
); );
}; };

@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'; 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 { ReloadOutlined, ArrowUpOutlined, ArrowDownOutlined, StarOutlined, DeleteOutlined, BellOutlined } from '@ant-design/icons';
import { fetchFuturesOverview, toggleWatchlist } from '../../store/futuresSlice'; import { fetchFuturesOverview, toggleWatchlist } from '../../store/futuresSlice';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@ -17,6 +18,7 @@ const Watchlist = () => {
const [pushModalVisible, setPushModalVisible] = useState(false); const [pushModalVisible, setPushModalVisible] = useState(false);
const [currentFuture, setCurrentFuture] = useState(null); const [currentFuture, setCurrentFuture] = useState(null);
const [pushForm] = Form.useForm(); const [pushForm] = Form.useForm();
const [messageApi, contextHolder] = message.useMessage();
useEffect(() => { useEffect(() => {
dispatch(fetchFuturesOverview()); dispatch(fetchFuturesOverview());
@ -24,7 +26,7 @@ const Watchlist = () => {
const handleRefresh = () => { const handleRefresh = () => {
dispatch(fetchFuturesOverview()); dispatch(fetchFuturesOverview());
message.success('数据已刷新'); messageApi.success('数据已刷新');
}; };
const handleFutureClick = (future) => { const handleFutureClick = (future) => {
@ -33,7 +35,7 @@ const Watchlist = () => {
const handleToggleWatchlist = (future) => { const handleToggleWatchlist = (future) => {
dispatch(toggleWatchlist(future.code)); dispatch(toggleWatchlist(future.code));
message.success(future.isInWatchlist ? '已从自选移除' : '已添加到自选'); messageApi.success(future.isInWatchlist ? '已从自选移除' : '已添加到自选');
}; };
const openPushConfig = (future, e) => { const openPushConfig = (future, e) => {
@ -46,7 +48,7 @@ const Watchlist = () => {
console.log('消息推送配置保存:', values); console.log('消息推送配置保存:', values);
// //
setPushModalVisible(false); setPushModalVisible(false);
message.success('消息推送配置已保存'); messageApi.success('消息推送配置已保存');
}; };
const getChangeColor = (changePercent) => { const getChangeColor = (changePercent) => {
@ -78,7 +80,9 @@ const Watchlist = () => {
const watchlistData = getWatchlistData(); const watchlistData = getWatchlistData();
return ( return (
<div className="watchlist"> <>
{contextHolder}
<div className="watchlist">
{/* 页面头部 */} {/* 页面头部 */}
<div className="watchlist-header"> <div className="watchlist-header">
<h2>自选合约</h2> <h2>自选合约</h2>
@ -351,7 +355,8 @@ const Watchlist = () => {
</Card> </Card>
</Form> </Form>
</Modal> </Modal>
</div> </div>
</>
); );
}; };

@ -3,44 +3,65 @@
// 期货品种列表 // 期货品种列表
export const futuresList = [ export const futuresList = [
// 金属类 // 金属类
{ code: 'AU', name: '金', type: '金属' }, { code: 'AU', name: '黄金', type: '金属', exchange: 'SHFE' },
{ code: 'AG', name: '银', type: '金属' }, { code: 'AG', name: '白银', type: '金属', exchange: 'SHFE' },
{ code: 'CU', name: '铜', type: '金属' }, { code: 'CU', name: '铜', type: '金属', exchange: 'SHFE' },
{ code: 'NI', name: '镍', type: '金属' }, { code: 'NI', name: '镍', type: '金属', exchange: 'SHFE' },
{ code: 'SN', name: '锡', type: '金属' }, { code: 'SN', name: '锡', type: '金属', exchange: 'SHFE' },
{ code: 'AL', name: '铝', type: '金属' }, { code: 'AL', name: '铝', type: '金属', exchange: 'SHFE' },
{ code: 'ZN', name: '锌', type: '金属' }, { code: 'ZN', name: '锌', type: '金属', exchange: 'SHFE' },
{ code: 'PB', name: '铅', type: '金属', exchange: 'SHFE' },
// 建材类 // 建材类
{ code: 'FG', name: '玻璃', type: '建材' }, { code: 'FG', name: '玻璃', type: '建材', exchange: 'CZCE' },
{ code: 'SJS', name: '烧碱', type: '建材' }, { code: 'LY', name: '烧碱', type: '建材', exchange: 'CZCE' },
{ code: 'SCA', name: '纯碱', type: '建材' }, { code: 'SA', name: '纯碱', type: '建材', exchange: 'CZCE' },
{ code: 'JM', name: '焦煤', type: '建材' }, { code: 'JM', name: '焦煤', type: '建材', exchange: 'DCE' },
{ code: 'RB', name: '螺纹钢', type: '建材' }, { code: 'RB', name: '螺纹钢', type: '建材', exchange: 'SHFE' },
{ code: 'ALO', name: '氧化铝', type: '建材' }, { code: 'ALO', name: '氧化铝', type: '建材', exchange: 'SHFE' },
{ code: 'HC', name: '热轧卷板', type: '建材', exchange: 'SHFE' },
// 能源化工类 // 能源化工类
{ code: 'MA', name: '甲醇', type: '能源化工' }, { code: 'MA', name: '甲醇', type: '能源化工', exchange: 'CZCE' },
{ code: 'PVC', name: 'PVC', type: '能源化工' }, { code: 'V', name: 'PVC', type: '能源化工', exchange: 'DCE' },
{ code: 'FU', name: '燃油', type: '能源化工' }, { code: 'FU', name: '燃油', type: '能源化工', exchange: 'SHFE' },
{ code: 'SC', name: '原油', type: '能源化工' }, { code: 'SC', name: '原油', type: '能源化工', exchange: 'INE' },
{ code: 'L', name: '橡胶', type: '能源化工' }, { code: 'RU', name: '橡胶', type: '能源化工', exchange: 'SHFE' },
{ code: 'NR', name: '20号胶', type: '能源化工' }, { code: 'BR', name: '合成橡胶', type: '能源化工', exchange: 'DCE' },
{ code: 'BU', name: '沥青', type: '能源化工' }, { code: 'NR', name: '20号胶', type: '能源化工', exchange: 'SHFE' },
{ code: 'LU', name: '低硫燃油', type: '能源化工' }, { 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: 'LI', name: '碳酸锂', type: '新能源', exchange: 'SHFE' },
{ code: 'SI', name: '工业硅', type: '新能源' }, { code: 'SI', name: '工业硅', type: '新能源', exchange: 'GEM' },
{ code: 'PGS', name: '多晶硅', type: '新能源' }, { code: 'SP', name: '多晶硅', type: '新能源', exchange: 'GEM' },
// 金融类 // 金融类
{ code: 'IC', name: '中证500', type: '金融' }, { code: 'IM', name: '中证1000', type: '金融', exchange: 'CFFEX' },
{ code: 'IM', name: '中证1000', type: '金融' }, { code: 'IC', name: '中证500', type: '金融', exchange: 'CFFEX' },
{ code: 'IH', name: '上证50', type: '金融' } { 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' }
]; ];
// 生成随机数据的工具函数 // 生成随机数据的工具函数

Loading…
Cancel
Save