stock-agent/web/static/js/app.js
2025-12-28 10:12:30 +08:00

862 lines
29 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 全局状态管理
const AppState = {
currentTab: 'screening',
loading: false
};
// 全局图表实例管理
const ChartInstances = {
scoringChart: null,
industryChart: null
};
// DOM 加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
initializeApp();
});
// 应用初始化
function initializeApp() {
setupNavigation();
setupEventListeners();
setupRangeSliders();
}
// 导航设置
function setupNavigation() {
const navLinks = document.querySelectorAll('.nav-link');
const tabContents = document.querySelectorAll('.tab-content');
navLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const targetTab = link.getAttribute('data-tab');
// 更新导航状态
navLinks.forEach(nav => nav.classList.remove('active'));
link.classList.add('active');
// 显示对应内容
tabContents.forEach(content => content.classList.remove('active'));
document.getElementById(targetTab).classList.add('active');
AppState.currentTab = targetTab;
});
});
}
// 事件监听器设置
function setupEventListeners() {
// 股票筛选
document.getElementById('screening-btn').addEventListener('click', handleScreening);
// 综合分析
document.getElementById('analysis-btn').addEventListener('click', handleComprehensiveAnalysis);
// 键盘事件
document.addEventListener('keydown', handleKeyPress);
}
// 范围滑块设置
function setupRangeSliders() {
// 移除范围滑块设置,因为筛选页面不再使用滑块
}
// 股票筛选处理
let isScreeningInProgress = false; // 防重复提交标志
async function handleScreening() {
// 防止重复点击
if (isScreeningInProgress) {
showToast('筛选正在进行中,请稍候...', 'warning');
return;
}
const minScore = 60; // 固定最低评分为60分
const limit = 50; // 固定结果数量为50个
// 综合筛选:使用三种策略综合评估
const data = { strategy: 'comprehensive', min_score: minScore, limit };
isScreeningInProgress = true;
const screeningBtn = document.getElementById('screening-btn');
const originalText = screeningBtn.innerHTML;
screeningBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 筛选中...';
screeningBtn.disabled = true;
showLoading('正在进行综合筛选...');
try {
const response = await fetch('/api/screen', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
if (response.ok) {
displayScreeningResults(result.results);
showToast('筛选成功!', 'success');
} else {
throw new Error(result.error || '筛选失败');
}
} catch (error) {
showToast(`筛选失败: ${error.message}`, 'error');
} finally {
isScreeningInProgress = false;
screeningBtn.innerHTML = originalText;
screeningBtn.disabled = false;
hideLoading();
}
}
// 显示筛选结果
function displayScreeningResults(results) {
const resultsSection = document.getElementById('screening-results');
const tableContainer = document.getElementById('screening-table');
if (!results || results.length === 0) {
tableContainer.innerHTML = '<p class="text-center">未找到符合条件的股票</p>';
resultsSection.style.display = 'block';
return;
}
// 检查是否是综合评分结果
const isComprehensive = results[0].value_score !== undefined;
// 创建表格
let headers, tableData;
if (isComprehensive) {
headers = ['股票代码', '名称', '行业', '综合评分', '价值评分', '成长评分', '技术评分', '建议'];
tableData = results.map(stock => [
stock.ts_code,
stock.name,
stock.industry,
stock.score.toFixed(1),
stock.value_score.toFixed(1),
stock.growth_score.toFixed(1),
stock.technical_score.toFixed(1),
stock.recommendation
]);
} else {
headers = ['股票代码', '名称', '行业', '评分', '建议'];
tableData = results.map(stock => [
stock.ts_code,
stock.name,
stock.industry,
stock.score.toFixed(1),
stock.recommendation
]);
}
const table = createDataTable(headers, tableData);
tableContainer.innerHTML = '';
tableContainer.appendChild(table);
// 创建图表
createScoringChart(results.slice(0, 10));
createIndustryChart(results);
resultsSection.style.display = 'block';
resultsSection.scrollIntoView({ behavior: 'smooth' });
}
// 综合分析处理 - 合并多策略分析
async function handleComprehensiveAnalysis() {
const tsCodeInput = document.getElementById('stock-code').value.trim();
if (!tsCodeInput) {
showToast('请输入股票代码', 'warning');
return;
}
// 格式化股票代码:自动添加交易所后缀
const tsCode = formatStockCode(tsCodeInput);
const strategies = ['value', 'growth', 'technical'];
const results = [];
showLoading('正在进行综合分析...');
try {
// 并行获取三种策略的分析结果
for (const strategy of strategies) {
const response = await fetch('/api/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ts_code: tsCode, strategy })
});
const result = await response.json();
if (response.ok) {
results.push({
strategy: getStrategyName(strategy),
rawStrategy: strategy,
score: result.score,
recommendation: result.recommendation,
data: result
});
}
}
if (results.length > 0) {
displayComprehensiveResults(results, tsCode);
showToast('综合分析完成!', 'success');
} else {
throw new Error('无法获取分析数据');
}
} catch (error) {
showToast(`分析失败: ${error.message}`, 'error');
} finally {
hideLoading();
}
}
// 显示股票基本信息
function displayStockBasicInfo(results, tsCode) {
// 从价值策略结果中获取基本信息(因为价值策略通常有最完整的财务数据)
const valueData = results.find(r => r.rawStrategy === 'value') || results[0];
const stockData = valueData.data;
// 显示股票代码
document.getElementById('stock-code-value').textContent = tsCode;
// 显示股票名称
const stockName = stockData.name || stockData.stock_name || '--';
document.getElementById('stock-name-value').textContent = stockName;
// 显示行业信息
const industry = stockData.industry || '--';
document.getElementById('stock-industry-value').textContent = industry;
// 显示当前价格
const currentPrice = stockData.current_price || stockData.price;
if (currentPrice) {
document.getElementById('stock-price-value').textContent = `¥${currentPrice.toFixed(2)}`;
} else {
document.getElementById('stock-price-value').textContent = '--';
}
// 显示市值
const marketCap = stockData.market_cap;
if (marketCap) {
const marketCapBillion = (marketCap / 100000000).toFixed(2);
document.getElementById('stock-market-cap-value').textContent = `${marketCapBillion}亿`;
} else {
document.getElementById('stock-market-cap-value').textContent = '--';
}
// 显示市盈率
const peRatio = stockData.financial_ratios?.pe_ratio;
if (peRatio && peRatio !== Infinity && peRatio > 0) {
document.getElementById('stock-pe-value').textContent = peRatio.toFixed(2);
} else {
document.getElementById('stock-pe-value').textContent = '--';
}
}
// 显示综合分析结果
function displayComprehensiveResults(results, tsCode) {
const resultsSection = document.getElementById('analysis-results');
// 显示股票基本信息
displayStockBasicInfo(results, tsCode);
// 计算综合指标
const avgScore = results.reduce((sum, r) => sum + r.score, 0) / results.length;
const bestStrategy = results.reduce((best, current) =>
current.score > best.score ? current : best
);
// 综合建议逻辑
const buyCount = results.filter(r => r.recommendation === 'BUY').length;
const holdCount = results.filter(r => r.recommendation === 'HOLD').length;
let finalRecommendation;
let confidenceLevel;
if (buyCount >= 2) {
finalRecommendation = 'BUY';
confidenceLevel = buyCount === 3 ? 'HIGH' : 'MEDIUM';
} else if (buyCount + holdCount >= 2) {
finalRecommendation = 'HOLD';
confidenceLevel = 'MEDIUM';
} else {
finalRecommendation = 'SELL';
confidenceLevel = results.filter(r => r.recommendation === 'SELL').length >= 2 ? 'MEDIUM' : 'LOW';
}
// 显示综合投资评级
displayInvestmentRating(avgScore, bestStrategy, finalRecommendation, confidenceLevel);
// 显示分析总结(包含策略详情表格)
displayAnalysisSummary(results, avgScore, finalRecommendation, bestStrategy);
// 显示财务指标 (使用价值策略的数据)
const valueData = results.find(r => r.rawStrategy === 'value');
if (valueData && valueData.data.financial_ratios) {
displayFinancialRatios(valueData.data.financial_ratios);
}
// 检查是否有技术分析的交易信号
const technicalData = results.find(r => r.rawStrategy === 'technical');
if (technicalData && technicalData.data.trading_signals) {
displayTradingSignals(technicalData.data.trading_signals);
document.getElementById('trading-signals-section').style.display = 'block';
} else {
document.getElementById('trading-signals-section').style.display = 'none';
}
resultsSection.style.display = 'block';
resultsSection.scrollIntoView({ behavior: 'smooth' });
}
// 显示综合投资评级
function displayInvestmentRating(avgScore, bestStrategy, finalRecommendation, confidenceLevel) {
// 显示综合评级指标
document.getElementById('best-strategy-value').textContent = bestStrategy.strategy;
document.getElementById('avg-score-value').textContent = avgScore.toFixed(1) + '分';
document.getElementById('final-recommendation-value').textContent = finalRecommendation;
document.getElementById('confidence-level-value').textContent =
confidenceLevel === 'HIGH' ? '高' :
confidenceLevel === 'MEDIUM' ? '中' : '低';
}
// 显示分析总结
function displayAnalysisSummary(results, avgScore, finalRecommendation, bestStrategy) {
const container = document.getElementById('summary-content');
let summaryHtml = `
<p><strong>综合评分:</strong>${avgScore.toFixed(1)}分</p>
<p><strong>投资建议:</strong>${finalRecommendation}</p>
<p><strong>最佳策略:</strong>${bestStrategy.strategy} (${bestStrategy.score.toFixed(1)}分)</p>
<br>
<p><strong>各策略分析:</strong></p>
<ul>
`;
results.forEach(result => {
let analysis = '';
if (result.score >= 65) {
analysis = '表现优秀,符合投资标准';
} else if (result.score >= 45) {
analysis = '表现一般,可考虑关注';
} else {
analysis = '表现较差,需谨慎考虑';
}
summaryHtml += `<li><strong>${result.strategy}</strong>${result.score.toFixed(1)}分,${analysis}</li>`;
});
summaryHtml += '</ul>';
// 添加投资建议说明
if (finalRecommendation === 'BUY') {
summaryHtml += '<p style="color: #22c55e; margin-top: 1rem;"><strong>建议买入:</strong>多数策略看好,具有较好投资价值</p>';
} else if (finalRecommendation === 'HOLD') {
summaryHtml += '<p style="color: #f59e0b; margin-top: 1rem;"><strong>建议持有:</strong>策略结果分化,建议持有观望</p>';
} else {
summaryHtml += '<p style="color: #ef4444; margin-top: 1rem;"><strong>建议卖出:</strong>多数策略不看好,存在较大风险</p>';
}
// 添加策略详细评分表格
summaryHtml += `
<div style="margin-top: 1.5rem;">
<div id="strategy-comparison-table-inline"></div>
</div>
`;
container.innerHTML = summaryHtml;
// 创建策略对比表格
createStrategyComparisonTable(results, 'strategy-comparison-table-inline');
}
// 创建策略对比表格
function createStrategyComparisonTable(results, containerId = 'strategy-comparison-table') {
const container = document.getElementById(containerId);
const headers = ['投资策略', '评分', '投资建议', '策略特点'];
const rows = results.map(r => [
r.strategy,
r.score.toFixed(1) + '分',
r.recommendation,
getStrategyDescription(r.rawStrategy)
]);
const table = createDataTable(headers, rows);
container.innerHTML = '';
container.appendChild(table);
}
// 获取策略描述
function getStrategyDescription(strategy) {
const descriptions = {
'value': '注重估值合理性,适合稳健投资',
'growth': '关注成长潜力,适合追求高收益',
'technical': '基于技术指标,适合短期交易'
};
return descriptions[strategy] || '';
}
// 显示财务指标
function displayFinancialRatios(ratios) {
const container = document.getElementById('financial-ratios');
const ratioItems = [
{ key: 'roe', label: 'ROE (净资产收益率)', suffix: '%' },
{ key: 'roa', label: 'ROA (总资产收益率)', suffix: '%' },
{ key: 'gross_margin', label: '毛利率', suffix: '%' },
{ key: 'net_margin', label: '净利率', suffix: '%' },
{ key: 'current_ratio', label: '流动比率', suffix: '' },
{ key: 'debt_ratio', label: '负债比率', suffix: '%' }
];
const grid = document.createElement('div');
grid.className = 'metrics-grid';
ratioItems.forEach(item => {
if (ratios[item.key] !== undefined) {
const card = document.createElement('div');
card.className = 'metric-card';
card.innerHTML = `
<div class="metric-value">${ratios[item.key].toFixed(2)}${item.suffix}</div>
<div class="metric-label">${item.label}</div>
`;
grid.appendChild(card);
}
});
container.innerHTML = '';
container.appendChild(grid);
}
// 显示交易信号分析
function displayTradingSignals(tradingSignals) {
const entrySignals = tradingSignals.entry_signals || {};
const exitSignals = tradingSignals.exit_signals || {};
const stopLossTakeProfit = tradingSignals.stop_loss_take_profit || {};
const tradingAdvice = tradingSignals.trading_advice || {};
const marketTiming = tradingSignals.market_timing || {};
// 显示主要信号指标
document.getElementById('entry-signal-value').textContent =
getSignalDisplayText(entrySignals.overall_signal) + ` (${entrySignals.signal_strength || 0}%)`;
document.getElementById('exit-signal-value').textContent =
getSignalDisplayText(exitSignals.overall_signal) + ` (${exitSignals.signal_strength || 0}%)`;
document.getElementById('trading-action-value').textContent =
getActionDisplayText(tradingAdvice.action);
document.getElementById('risk-level-value').textContent =
getRiskDisplayText(tradingAdvice.risk_assessment);
// 显示买入价格和卖出价格建议
const currentPrice = tradingSignals.current_price || 0;
const entryPriceData = tradingSignals.entry_price || {};
const exitPriceData = tradingSignals.exit_price || {};
// 根据操作建议显示相应的价格
const action = tradingAdvice.action;
document.getElementById('entry-price-value').textContent =
(action === 'BUY' || action === 'CONSIDER_BUY') && entryPriceData.recommended ?
`¥${entryPriceData.recommended}` :
(action === 'BUY' || action === 'CONSIDER_BUY') && currentPrice > 0 ?
`¥${currentPrice.toFixed(2)}` : '--';
document.getElementById('exit-price-value').textContent =
(action === 'SELL') && exitPriceData.recommended ?
`¥${exitPriceData.recommended}` :
(action === 'SELL') && currentPrice > 0 ?
`¥${currentPrice.toFixed(2)}` : '--';
// 显示止盈止损信息(只有在非观望状态时才显示)
const stopLoss = stopLossTakeProfit.stop_loss || {};
const takeProfit = stopLossTakeProfit.take_profit || {};
const isWaitAction = tradingAdvice.action === 'WAIT';
document.getElementById('stop-loss-value').textContent =
(isWaitAction || !stopLoss.recommended) ? '--' : `¥${stopLoss.recommended}`;
document.getElementById('take-profit-value').textContent =
(isWaitAction || !takeProfit.recommended) ? '--' : `¥${takeProfit.recommended}`;
document.getElementById('risk-reward-ratio-value').textContent =
(isWaitAction || !stopLossTakeProfit.risk_reward_ratio) ? '--' : `1:${stopLossTakeProfit.risk_reward_ratio}`;
document.getElementById('position-size-value').textContent =
(isWaitAction || !stopLossTakeProfit.position_sizing_suggestion) ? '--' :
`${stopLossTakeProfit.position_sizing_suggestion.suggested_position}%`;
// 显示详细信号分析
displaySignalsDetails(entrySignals, exitSignals, tradingAdvice, marketTiming);
}
// 显示信号详情
function displaySignalsDetails(entrySignals, exitSignals, tradingAdvice, marketTiming) {
const container = document.getElementById('signals-content');
let detailsHtml = '<div class="signals-analysis">';
// 买入信号详情
if (entrySignals.entry_reasons && entrySignals.entry_reasons.length > 0) {
detailsHtml += '<div class="signal-section">';
detailsHtml += '<h5 style="color: #22c55e; margin-bottom: 0.5rem;">📈 买入信号原因</h5>';
detailsHtml += '<ul>';
entrySignals.entry_reasons.forEach(reason => {
detailsHtml += `<li>${reason}</li>`;
});
detailsHtml += '</ul></div>';
}
// 卖出信号详情
if (exitSignals.exit_reasons && exitSignals.exit_reasons.length > 0) {
detailsHtml += '<div class="signal-section">';
detailsHtml += '<h5 style="color: #ef4444; margin-bottom: 0.5rem;">📉 卖出信号原因</h5>';
detailsHtml += '<ul>';
exitSignals.exit_reasons.forEach(reason => {
detailsHtml += `<li>${reason}</li>`;
});
detailsHtml += '</ul></div>';
}
// 交易建议详情
if (tradingAdvice.reasoning && tradingAdvice.reasoning.length > 0) {
detailsHtml += '<div class="signal-section">';
detailsHtml += '<h5 style="color: #3b82f6; margin-bottom: 0.5rem;">🎯 交易建议理由</h5>';
detailsHtml += '<ul>';
tradingAdvice.reasoning.forEach(reason => {
detailsHtml += `<li>${reason}</li>`;
});
detailsHtml += '</ul></div>';
}
// 市场时机分析
if (marketTiming.recommendations && marketTiming.recommendations.length > 0) {
detailsHtml += '<div class="signal-section">';
detailsHtml += '<h5 style="color: #f59e0b; margin-bottom: 0.5rem;">⏰ 市场时机分析</h5>';
detailsHtml += `<p><strong>市场状态:</strong> ${getMarketConditionText(marketTiming.market_condition)}</p>`;
detailsHtml += `<p><strong>流动性:</strong> ${getLiquidityText(marketTiming.liquidity)}</p>`;
detailsHtml += `<p><strong>趋势强度:</strong> ${getTrendStrengthText(marketTiming.trend_strength)}</p>`;
detailsHtml += '<ul>';
marketTiming.recommendations.forEach(rec => {
detailsHtml += `<li>${rec}</li>`;
});
detailsHtml += '</ul></div>';
}
// 风险提示
const warningFlags = entrySignals.warning_flags || [];
const riskFlags = exitSignals.risk_flags || [];
if (warningFlags.length > 0 || riskFlags.length > 0) {
detailsHtml += '<div class="signal-section">';
detailsHtml += '<h5 style="color: #dc2626; margin-bottom: 0.5rem;">⚠️ 风险提示</h5>';
detailsHtml += '<ul>';
[...warningFlags, ...riskFlags].forEach(flag => {
detailsHtml += `<li style="color: #dc2626;">${flag}</li>`;
});
detailsHtml += '</ul></div>';
}
detailsHtml += '</div>';
container.innerHTML = detailsHtml;
}
// 信号显示文本转换函数
function getSignalDisplayText(signal) {
const signalMap = {
'STRONG_BUY': '强烈买入',
'BUY': '买入',
'WEAK_BUY': '弱买入',
'NEUTRAL': '中性',
'WEAK_SELL': '弱卖出',
'SELL': '卖出',
'STRONG_SELL': '强烈卖出'
};
return signalMap[signal] || '未知';
}
function getActionDisplayText(action) {
const actionMap = {
'BUY': '买入',
'SELL': '卖出',
'CONSIDER_BUY': '考虑买入',
'WAIT': '观望',
'HOLD': '持有'
};
return actionMap[action] || '观望';
}
function getRiskDisplayText(risk) {
const riskMap = {
'LOW': '低风险',
'MEDIUM': '中等风险',
'HIGH': '高风险'
};
return riskMap[risk] || '中等风险';
}
function getMarketConditionText(condition) {
const conditionMap = {
'NORMAL': '正常',
'VOLATILE': '高波动',
'QUIET': '平静'
};
return conditionMap[condition] || '正常';
}
function getLiquidityText(liquidity) {
const liquidityMap = {
'POOR': '流动性差',
'GOOD': '流动性良好',
'EXCELLENT': '流动性优秀'
};
return liquidityMap[liquidity] || '流动性良好';
}
function getTrendStrengthText(strength) {
const strengthMap = {
'WEAK': '趋势较弱',
'MEDIUM': '趋势中等',
'STRONG': '趋势较强'
};
return strengthMap[strength] || '趋势中等';
}
// 策略对比处理 - 已移除,合并到综合分析中
// 此函数已被 handleComprehensiveAnalysis 替代
// 显示对比结果 - 已移除,合并到综合分析中
// 此函数已被 displayComprehensiveResults 替代
// 工具函数
function formatStockCode(code) {
// 移除所有空格和非数字字符,保留原有后缀
const cleanCode = code.replace(/\s+/g, '').toUpperCase();
// 如果已经包含交易所后缀,直接返回
if (cleanCode.includes('.SZ') || cleanCode.includes('.SH')) {
return cleanCode;
}
// 只包含数字的情况,自动判断交易所
const numericCode = cleanCode.replace(/[^0-9]/g, '');
if (numericCode.length === 6) {
// 深交所000xxx, 002xxx, 300xxx
if (numericCode.startsWith('000') || numericCode.startsWith('002') || numericCode.startsWith('300')) {
return numericCode + '.SZ';
}
// 上交所600xxx, 601xxx, 603xxx, 688xxx
else if (numericCode.startsWith('60') || numericCode.startsWith('688')) {
return numericCode + '.SH';
}
}
// 默认返回原输入加.SZ深交所较多
return numericCode + '.SZ';
}
function getStrategyName(strategy) {
const names = {
'value': '价值投资',
'growth': '成长投资',
'technical': '技术分析'
};
return names[strategy] || strategy;
}
function getStrategyIcon(strategy) {
const icons = {
'价值投资': '💰',
'成长投资': '🚀',
'技术分析': '📈'
};
return icons[strategy] || '📊';
}
// 创建数据表格
function createDataTable(headers, rows) {
const table = document.createElement('table');
table.className = 'data-table';
// 创建表头
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
headers.forEach(header => {
const th = document.createElement('th');
th.textContent = header;
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
// 创建表体
const tbody = document.createElement('tbody');
rows.forEach(row => {
const tr = document.createElement('tr');
row.forEach(cell => {
const td = document.createElement('td');
td.textContent = cell;
tr.appendChild(td);
});
tbody.appendChild(tr);
});
table.appendChild(tbody);
return table;
}
// 图表创建函数
function createScoringChart(data) {
const ctx = document.getElementById('scoring-chart').getContext('2d');
// 销毁已存在的图表
if (ChartInstances.scoringChart) {
ChartInstances.scoringChart.destroy();
}
ChartInstances.scoringChart = new Chart(ctx, {
type: 'bar',
data: {
labels: data.map(item => item.name),
datasets: [{
label: '评分',
data: data.map(item => item.score),
backgroundColor: '#000000', /* 纯黑色 */
borderColor: '#000000',
borderWidth: 1,
borderRadius: 4
}]
},
options: {
responsive: true,
plugins: {
title: { display: true, text: '股票评分排行' },
legend: { display: false }
},
scales: {
y: { beginAtZero: true, max: 100 }
}
}
});
}
function createIndustryChart(data) {
const ctx = document.getElementById('industry-chart').getContext('2d');
// 销毁已存在的图表
if (ChartInstances.industryChart) {
ChartInstances.industryChart.destroy();
}
const industries = {};
data.forEach(item => {
industries[item.industry] = (industries[item.industry] || 0) + 1;
});
ChartInstances.industryChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: Object.keys(industries),
datasets: [{
data: Object.values(industries),
backgroundColor: [
'#000000', '#333333', '#666666', '#999999',
'#cccccc', '#444444', '#777777', '#aaaaaa'
]
}]
},
options: {
responsive: true,
plugins: {
title: { display: true, text: '行业分布' }
}
}
});
}
// UI 辅助函数
function showLoading(message = '加载中...') {
const loading = document.getElementById('loading');
loading.querySelector('p').textContent = message;
loading.style.display = 'flex';
AppState.loading = true;
}
function hideLoading() {
document.getElementById('loading').style.display = 'none';
AppState.loading = false;
}
function showToast(message, type = 'info') {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
const icon = getToastIcon(type);
toast.innerHTML = `
<div style="display: flex; align-items: center; gap: 8px;">
<span>${icon}</span>
<span>${message}</span>
</div>
`;
container.appendChild(toast);
// 自动移除
setTimeout(() => {
toast.style.animation = 'slideOutRight 0.3s ease';
setTimeout(() => container.removeChild(toast), 300);
}, 3000);
}
function getToastIcon(type) {
const icons = {
success: '✅',
error: '❌',
warning: '⚠️',
info: ''
};
return icons[type] || icons.info;
}
// 键盘快捷键处理
function handleKeyPress(e) {
if (AppState.loading) return;
// Ctrl + 数字键切换标签
if (e.ctrlKey) {
switch(e.key) {
case '1':
e.preventDefault();
document.querySelector('[data-tab="screening"]').click();
break;
case '2':
e.preventDefault();
document.querySelector('[data-tab="analysis"]').click();
break;
}
}
// Enter 键执行当前标签的主要操作
if (e.key === 'Enter' && e.ctrlKey) {
e.preventDefault();
switch(AppState.currentTab) {
case 'screening':
document.getElementById('screening-btn').click();
break;
case 'analysis':
document.getElementById('analysis-btn').click();
break;
}
}
}