diff --git a/docker-compose.yml b/docker-compose.yml index 761a7e4..17b2aea 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,7 @@ services: - FLASK_ENV=production - PYTHONPATH=/app - TZ=Asia/Shanghai + - OPERATION_KEY=${OPERATION_KEY:-9257} # MySQL连接配置 (可通过环境变量覆盖) - MYSQL_HOST=${MYSQL_HOST:-cd-cynosdbmysql-grp-7kdd8qe4.sql.tencentcdb.com} - MYSQL_PORT=${MYSQL_PORT:-26558} @@ -39,6 +40,7 @@ services: environment: - PYTHONPATH=/app - TZ=Asia/Shanghai + - OPERATION_KEY=${OPERATION_KEY:-9257} - LOG_LEVEL=${LOG_LEVEL:-INFO} - MARKET_SCAN_STOCKS=${MARKET_SCAN_STOCKS:-200} # MySQL连接配置 diff --git a/web/mysql_app.py b/web/mysql_app.py index 25c863d..21a54d8 100644 --- a/web/mysql_app.py +++ b/web/mysql_app.py @@ -5,6 +5,7 @@ AI 智能选股大师 MySQL版本 Web 展示界面 """ import sys +import os from pathlib import Path from flask import Flask, render_template, jsonify, request from datetime import datetime, date, timedelta @@ -22,6 +23,9 @@ from loguru import logger app = Flask(__name__) app.secret_key = 'trading_ai_mysql_secret_key_2023' +# 操作验证密钥(放在导入后立即定义) +OPERATION_KEY = os.environ.get('OPERATION_KEY', '9257') + # 初始化组件 db_manager = MySQLDatabaseManager() config_loader = ConfigLoader() @@ -172,6 +176,17 @@ def api_stats(): def api_clear_signals(): """API接口 - 清空信号数据""" try: + # 获取密钥验证 + operation_key = request.json.get('operation_key', '') if request.is_json else request.form.get('operation_key', '') + + # 验证密钥 + if operation_key != OPERATION_KEY: + logger.warning(f"清空信号操作密钥验证失败: {operation_key}") + return jsonify({ + 'success': False, + 'error': '操作密钥验证失败,请输入正确的验证密钥' + }) + # 获取清空范围参数 days = request.json.get('days', 7) if request.is_json else int(request.form.get('days', 7)) strategy_name = request.json.get('strategy_name', '') if request.is_json else request.form.get('strategy_name', '') @@ -204,6 +219,17 @@ def api_run_analysis(): import threading from datetime import datetime + # 获取密钥验证 + operation_key = request.json.get('operation_key', '') if request.is_json else request.form.get('operation_key', '') + + # 验证密钥 + if operation_key != OPERATION_KEY: + logger.warning(f"立即分析操作密钥验证失败: {operation_key}") + return jsonify({ + 'success': False, + 'error': '操作密钥验证失败,请输入正确的验证密钥' + }) + # 检查是否已有分析在运行 if analysis_status['is_running']: return jsonify({ diff --git a/web/static/css/style.css b/web/static/css/style.css index 6ef0257..777a05a 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -1110,4 +1110,367 @@ footer .text-muted { font-size: 0.65rem; padding: 0.2rem 0.4rem; } +} + +/* ========== 自定义模态框样式 ========== */ + +/* 模态框整体美化 */ +.modal-content { + border: none !important; + border-radius: var(--radius-lg) !important; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25) !important; + backdrop-filter: blur(10px); + animation: modalFadeIn 0.3s ease-out; +} + +@keyframes modalFadeIn { + from { + opacity: 0; + transform: scale(0.9) translateY(-10px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +/* 模态框背景遮罩美化 */ +.modal-backdrop { + background: linear-gradient(135deg, rgba(0, 0, 0, 0.4), rgba(37, 99, 235, 0.1)) !important; + backdrop-filter: blur(5px); +} + +/* 密钥输入模态框 */ +#keyModal .modal-header { + background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%); + border-radius: var(--radius-lg) var(--radius-lg) 0 0; + border-bottom: none; +} + +#keyModal .modal-title { + font-weight: 600; + font-size: 1.125rem; +} + +#keyModal .modal-body { + background: linear-gradient(135deg, var(--bg-accent) 0%, var(--bg-secondary) 100%); +} + +#keyModal #operationKey { + border: 2px solid var(--border-color); + border-radius: var(--radius-md); + background: var(--bg-accent); + font-size: 1.125rem; + font-weight: 600; + letter-spacing: 0.1em; + transition: all 0.3s ease; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.05); +} + +#keyModal #operationKey:focus { + border-color: var(--primary-color); + box-shadow: 0 0 0 4px var(--primary-lighter), inset 0 2px 4px rgba(0, 0, 0, 0.05); + transform: translateY(-1px); +} + +#keyModal #operationKey.is-invalid { + border-color: var(--danger-color); + box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.1); + animation: shake 0.5s ease-in-out; +} + +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-5px); } + 75% { transform: translateX(5px); } +} + +/* 确认操作模态框 */ +#confirmModal .modal-header.bg-danger { + background: linear-gradient(135deg, var(--danger-color) 0%, #dc2626 100%) !important; + color: white; +} + +#confirmModal .modal-header.bg-warning { + background: linear-gradient(135deg, var(--warning-color) 0%, #d97706 100%) !important; + color: white; +} + +#confirmModal .modal-header.bg-info { + background: linear-gradient(135deg, var(--info-color) 0%, #0891b2 100%) !important; + color: white; +} + +#confirmModal .modal-body { + background: linear-gradient(135deg, var(--bg-accent) 0%, var(--bg-secondary) 100%); +} + +#confirmModal #confirmIcon.text-danger { + color: var(--danger-color) !important; + animation: pulse 2s infinite; +} + +#confirmModal #confirmIcon.text-warning { + color: var(--warning-color) !important; + animation: pulse 2s infinite; +} + +#confirmModal #confirmIcon.text-info { + color: var(--info-color) !important; +} + +@keyframes pulse { + 0%, 100% { transform: scale(1); opacity: 1; } + 50% { transform: scale(1.05); opacity: 0.8; } +} + +/* 输入参数模态框 */ +#inputModal .modal-header { + background: linear-gradient(135deg, var(--info-color) 0%, #0891b2 100%); + border-radius: var(--radius-lg) var(--radius-lg) 0 0; +} + +#inputModal .modal-body { + background: linear-gradient(135deg, var(--bg-accent) 0%, var(--bg-secondary) 100%); +} + +#inputModal #inputValue { + border: 2px solid var(--border-color); + border-radius: var(--radius-md); + background: var(--bg-accent); + font-size: 1.25rem; + font-weight: 600; + transition: all 0.3s ease; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.05); +} + +#inputModal #inputValue:focus { + border-color: var(--info-color); + box-shadow: 0 0 0 4px rgba(6, 182, 212, 0.1), inset 0 2px 4px rgba(0, 0, 0, 0.05); + transform: translateY(-1px); +} + +#inputModal #inputValue.is-invalid { + border-color: var(--danger-color); + box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.1); + animation: shake 0.5s ease-in-out; +} + +/* 通知提示模态框 */ +#alertModal .modal-header.bg-success { + background: linear-gradient(135deg, var(--success-color) 0%, #059669 100%) !important; + color: white; +} + +#alertModal .modal-header.bg-error { + background: linear-gradient(135deg, var(--danger-color) 0%, #dc2626 100%) !important; + color: white; +} + +#alertModal .modal-header.bg-warning { + background: linear-gradient(135deg, var(--warning-color) 0%, #d97706 100%) !important; + color: white; +} + +#alertModal .modal-header.bg-info { + background: linear-gradient(135deg, var(--info-color) 0%, #0891b2 100%) !important; + color: white; +} + +#alertModal .modal-body { + background: linear-gradient(135deg, var(--bg-accent) 0%, var(--bg-secondary) 100%); +} + +#alertModal #alertIcon.text-success { + color: var(--success-color) !important; + animation: bounce 2s infinite; +} + +#alertModal #alertIcon.text-error { + color: var(--danger-color) !important; + animation: shake 1s ease-in-out; +} + +#alertModal #alertIcon.text-warning { + color: var(--warning-color) !important; + animation: pulse 2s infinite; +} + +#alertModal #alertIcon.text-info { + color: var(--info-color) !important; +} + +@keyframes bounce { + 0%, 20%, 50%, 80%, 100% { transform: translateY(0); } + 40% { transform: translateY(-8px); } + 60% { transform: translateY(-4px); } +} + +/* 模态框按钮美化 */ +.modal .btn { + border-radius: var(--radius-md); + padding: 0.75rem 1.5rem; + font-weight: 600; + font-size: 0.875rem; + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.modal .btn::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + background: rgba(255, 255, 255, 0.2); + border-radius: 50%; + transform: translate(-50%, -50%); + transition: width 0.6s, height 0.6s; +} + +.modal .btn:hover::before { + width: 300px; + height: 300px; +} + +.modal .btn-primary { + background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%); + border: none; + box-shadow: 0 4px 12px rgba(37, 99, 235, 0.4); +} + +.modal .btn-primary:hover { + background: linear-gradient(135deg, #1d4ed8 0%, var(--primary-color) 100%); + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba(37, 99, 235, 0.6); +} + +.modal .btn-danger { + background: linear-gradient(135deg, var(--danger-color) 0%, #dc2626 100%); + border: none; + box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4); +} + +.modal .btn-danger:hover { + background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%); + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba(239, 68, 68, 0.6); +} + +.modal .btn-info { + background: linear-gradient(135deg, var(--info-color) 0%, #0891b2 100%); + border: none; + box-shadow: 0 4px 12px rgba(6, 182, 212, 0.4); +} + +.modal .btn-info:hover { + background: linear-gradient(135deg, #0891b2 0%, #0e7490 100%); + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba(6, 182, 212, 0.6); +} + +.modal .btn-secondary { + background: linear-gradient(135deg, var(--secondary-color) 0%, #475569 100%); + border: none; + color: white; + box-shadow: 0 4px 12px rgba(100, 116, 139, 0.4); +} + +.modal .btn-secondary:hover { + background: linear-gradient(135deg, #475569 0%, #334155 100%); + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba(100, 116, 139, 0.6); +} + +/* 模态框输入框验证状态 */ +.modal .form-control.is-invalid { + border-color: var(--danger-color); + box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.1); +} + +.modal .form-control.is-valid { + border-color: var(--success-color); + box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.1); +} + +.modal .invalid-feedback { + display: block; + color: var(--danger-color); + font-size: 0.875rem; + font-weight: 500; + margin-top: 0.5rem; + animation: slideIn 0.3s ease-out; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* 模态框图标美化 */ +.modal i[style*="font-size: 3rem"] { + filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.1)); + transition: all 0.3s ease; +} + +.modal i[style*="font-size: 3rem"]:hover { + transform: scale(1.05); +} + +/* 模态框关闭按钮美化 */ +.modal .btn-close { + background: none; + border: none; + opacity: 0.8; + transition: all 0.3s ease; + border-radius: 50%; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; +} + +.modal .btn-close:hover { + opacity: 1; + background: rgba(255, 255, 255, 0.2); + transform: rotate(90deg); +} + +/* 模态框表单标签美化 */ +.modal .form-label { + color: var(--text-primary); + font-weight: 600; + font-size: 0.875rem; + margin-bottom: 0.5rem; +} + +.modal .form-text { + color: var(--text-muted); + font-size: 0.75rem; + margin-top: 0.25rem; +} + +/* 响应式模态框 */ +@media (max-width: 576px) { + .modal-dialog { + margin: 1rem; + } + + .modal .btn { + padding: 0.5rem 1rem; + font-size: 0.8rem; + } + + .modal i[style*="font-size: 3rem"] { + font-size: 2rem !important; + } } \ No newline at end of file diff --git a/web/templates/signals.html b/web/templates/signals.html index 16d2231..c376ccb 100644 --- a/web/templates/signals.html +++ b/web/templates/signals.html @@ -278,6 +278,127 @@ + + + +
+ + + + + + + + + {% endblock %} {% block extra_js %} @@ -298,41 +419,403 @@ window.location.search = urlParams.toString(); } - // 清空信号按钮 - $('#clearSignalsBtn').on('click', function() { - if (confirm('确认要清空最近7天的信号数据吗?此操作不可恢复!')) { - const btn = $(this); - const originalText = btn.html(); + // 全局变量用于存储操作回调 + let currentOperationCallback = null; + let currentInputCallback = null; + let pendingOperationKey = null; // 存储密钥 - btn.prop('disabled', true).html('清空中...'); + // ========== 自定义模态框函数 ========== - $.ajax({ - url: '/api/clear_signals', - method: 'POST', - contentType: 'application/json', - data: JSON.stringify({ - days: 7, - strategy_name: '' - }), - success: function(response) { - if (response.success) { - alert(`清空成功!删除了 ${response.deleted_count} 条信号记录`); - location.reload(); - } else { - alert('清空失败:' + response.error); - } - }, - error: function(xhr, status, error) { - alert('清空失败:网络错误或服务器异常'); - console.error('清空信号失败:', error); - }, - complete: function() { - btn.prop('disabled', false).html(originalText); - } - }); + // 显示验证密钥模态框 + function showKeyModal(title, message, callback) { + $('#keyModal .modal-title').text(title); + $('#keyModal p').text(message); + $('#operationKey').val('').removeClass('is-invalid'); + $('#keyError').text(''); + currentOperationCallback = callback; + new bootstrap.Modal($('#keyModal')).show(); + } + + // 显示确认操作模态框 + function showConfirmModal(title, message, type = 'warning', callback) { + const $header = $('#confirmHeader'); + const $icon = $('#confirmIcon'); + const $action = $('#confirmAction'); + + // 设置标题 + $('#confirmTitle').html(`${title}`); + $('#confirmMessage').text(message); + + // 根据类型设置样式 + $header.removeClass('bg-danger bg-warning bg-info').addClass(`bg-${type}`); + $icon.removeClass('text-danger text-warning text-info').addClass(`text-${type}`); + $action.removeClass('btn-danger btn-warning btn-info').addClass(`btn-${type}`); + + // 设置图标 + const icons = { + 'danger': 'fas fa-exclamation-triangle', + 'warning': 'fas fa-exclamation-triangle', + 'info': 'fas fa-info-circle' + }; + $icon.attr('class', `${icons[type] || icons.warning} text-${type}`).css('font-size', '3rem'); + + currentOperationCallback = callback; + new bootstrap.Modal($('#confirmModal')).show(); + } + + // 显示输入参数模态框 + function showInputModal(title, label, placeholder, helpText, callback) { + $('#inputTitle').html(`${title}`); + $('#inputLabel').text(label); + $('#inputValue').attr('placeholder', placeholder).val(placeholder).removeClass('is-invalid'); + $('#inputHelp').text(helpText); + $('#inputError').text(''); + currentInputCallback = callback; + new bootstrap.Modal($('#inputModal')).show(); + } + + // 显示通知提示模态框 + function showAlertModal(title, message, type = 'info', details = null) { + const $header = $('#alertHeader'); + const $icon = $('#alertIcon'); + const $confirm = $('#alertConfirm'); + + // 设置标题 + $('#alertTitle').html(`${title}`); + $('#alertMessage').text(message); + + // 根据类型设置样式 + const typeClass = type === 'error' ? 'error' : type; + $header.removeClass('bg-success bg-error bg-warning bg-info').addClass(`bg-${typeClass}`); + $icon.removeClass('text-success text-error text-warning text-info').addClass(`text-${typeClass}`); + + // 设置图标 + const icons = { + 'success': 'fas fa-check-circle', + 'error': 'fas fa-exclamation-circle', + 'warning': 'fas fa-exclamation-triangle', + 'info': 'fas fa-info-circle' + }; + $icon.attr('class', `${icons[type] || icons.info} text-${typeClass}`).css('font-size', '3rem'); + + // 显示详情 + if (details) { + $('#alertDetails').html(details).show(); + } else { + $('#alertDetails').hide(); + } + + new bootstrap.Modal($('#alertModal')).show(); + } + + // ========== 模态框事件处理 ========== + + // 验证密钥确认 + $(document).on('click', '#confirmKey', function() { + const key = $('#operationKey').val().trim(); + + if (!key) { + $('#operationKey').addClass('is-invalid'); + $('#keyError').text('请输入验证密钥'); + return; + } + + try { + const modalInstance = bootstrap.Modal.getInstance($('#keyModal')[0]); + if (modalInstance) { + modalInstance.hide(); + } else { + $('#keyModal').modal('hide'); + } + } catch (e) { + $('#keyModal').hide(); + } + + if (window.handleKeyConfirm) { + setTimeout(() => { + window.handleKeyConfirm(key); + }, 300); } }); + // 确认操作 + $(document).on('click', '#confirmAction', function() { + try { + const modalInstance = bootstrap.Modal.getInstance($('#confirmModal')[0]); + if (modalInstance) { + modalInstance.hide(); + } else { + $('#confirmModal').modal('hide'); + } + } catch (e) { + $('#confirmModal').hide(); + } + + if (window.handleConfirmAction) { + setTimeout(() => { + window.handleConfirmAction(); + }, 300); + } + }); + + // 输入参数确认 + $(document).on('click', '#confirmInput', function() { + const value = $('#inputValue').val().trim(); + // 如果输入为空,使用placeholder作为默认值 + const finalValue = value || $('#inputValue').attr('placeholder'); + + if (!finalValue) { + $('#inputValue').addClass('is-invalid'); + $('#inputError').text('请输入有效的参数值'); + return; + } + + try { + const modalInstance = bootstrap.Modal.getInstance($('#inputModal')[0]); + if (modalInstance) { + modalInstance.hide(); + } else { + $('#inputModal').modal('hide'); + } + } catch (e) { + $('#inputModal').hide(); + } + + if (window.handleInputConfirm) { + setTimeout(() => { + window.handleInputConfirm(finalValue); + }, 300); + } + }); + + // 回车键支持 + $('#operationKey').on('keypress', function(e) { + if (e.which === 13) $('#confirmKey').click(); + }); + + $('#inputValue').on('keypress', function(e) { + if (e.which === 13) $('#confirmInput').click(); + }); + + // 清空验证错误 + $('#operationKey').on('input', function() { + $(this).removeClass('is-invalid'); + $('#keyError').text(''); + }); + + $('#inputValue').on('input', function() { + $(this).removeClass('is-invalid'); + $('#inputError').text(''); + }); + + // 清空信号按钮 + $('#clearSignalsBtn').on('click', function() { + // 显示密钥输入模态框 + $('#keyModal .modal-title').text('操作验证'); + $('#keyModal p').text('请输入操作验证密钥以清空信号数据'); + $('#operationKey').val('').removeClass('is-invalid'); + $('#keyError').text(''); + + // 设置全局标识 + window.currentOperation = 'clear_signals'; + + new bootstrap.Modal($('#keyModal')).show(); + }); + + // 全局处理密钥确认 + window.handleKeyConfirm = function(operationKey) { + if (window.currentOperation === 'clear_signals') { + // 显示确认模态框 + $('#confirmModal .modal-title').html('确认清空信号'); + $('#confirmMessage').text('确认要清空最近7天的信号数据吗?此操作不可恢复!'); + $('#confirmHeader').removeClass('bg-danger bg-warning bg-info').addClass('bg-danger'); + $('#confirmIcon').removeClass('text-danger text-warning text-info').addClass('text-danger'); + $('#confirmAction').removeClass('btn-danger btn-warning btn-info').addClass('btn-danger'); + $('#confirmIcon').attr('class', 'fas fa-exclamation-triangle text-danger').css('font-size', '3rem'); + + // 存储密钥 + window.pendingOperationKey = operationKey; + window.currentOperation = 'confirm_clear_signals'; + + new bootstrap.Modal($('#confirmModal')).show(); + } else if (window.currentOperation === 'run_analysis') { + // 显示参数输入模态框 + $('#inputModal .modal-title').html('设置分析参数'); + $('#inputLabel').text('扫描股票数量'); + $('#inputValue').attr('placeholder', '200').val('200').removeClass('is-invalid'); + $('#inputHelp').text('请输入要扫描的股票数量 (范围: 1-1000)'); + $('#inputError').text(''); + + // 存储密钥 + window.pendingOperationKey = operationKey; + window.currentOperation = 'input_analysis_params'; + + new bootstrap.Modal($('#inputModal')).show(); + } + }; + + // 全局处理确认操作 + window.handleConfirmAction = function() { + if (window.currentOperation === 'confirm_clear_signals' && window.pendingOperationKey) { + executeClearSignals(window.pendingOperationKey); + window.currentOperation = null; + window.pendingOperationKey = null; + } else if (window.currentOperation === 'confirm_run_analysis' && window.pendingOperationKey && window.pendingStockCount) { + executeRunAnalysis(window.pendingOperationKey, window.pendingStockCount); + window.currentOperation = null; + window.pendingOperationKey = null; + window.pendingStockCount = null; + } + }; + + // 全局处理输入参数确认 + window.handleInputConfirm = function(inputValue) { + if (window.currentOperation === 'input_analysis_params' && window.pendingOperationKey) { + const count = parseInt(inputValue) || 200; + + if (count <= 0 || count > 1000) { + showAlertModal('参数错误', '股票数量必须在 1-1000 之间', 'warning'); + return; + } + + // 显示确认模态框 + $('#confirmModal .modal-title').html('确认分析'); + $('#confirmMessage').text(`确认要立即分析 ${count} 只热门股票吗?分析可能需要几分钟时间`); + $('#confirmHeader').removeClass('bg-danger bg-warning bg-info').addClass('bg-info'); + $('#confirmIcon').removeClass('text-danger text-warning text-info').addClass('text-info'); + $('#confirmAction').removeClass('btn-danger btn-warning btn-info').addClass('btn-info'); + $('#confirmIcon').attr('class', 'fas fa-info-circle text-info').css('font-size', '3rem'); + + // 存储参数 + window.pendingStockCount = count; + window.currentOperation = 'confirm_run_analysis'; + + new bootstrap.Modal($('#confirmModal')).show(); + } + }; + + // 执行清空信号的实际操作 + function executeClearSignals(operationKey) { + const btn = $('#clearSignalsBtn'); + const originalText = btn.html(); + + btn.prop('disabled', true).html('清空中...'); + + $.ajax({ + url: '/api/clear_signals', + method: 'POST', + contentType: 'application/json', + data: JSON.stringify({ + operation_key: operationKey, + days: 7, + strategy_name: '' + }), + success: function(response) { + if (response.success) { + showAlertModal( + '清空成功', + `成功删除了 ${response.deleted_count} 条信号记录`, + 'success' + ); + setTimeout(() => location.reload(), 2000); + } else { + // 检查是否是密钥验证失败 + if (response.error && response.error.includes('密钥验证失败')) { + showAlertModal('验证失败', '操作密钥验证失败,请检查密钥是否正确', 'warning'); + } else { + showAlertModal('清空失败', response.error || '未知错误', 'error'); + } + } + }, + error: function(xhr, status, error) { + let errorMessage = '网络错误或服务器异常'; + + // 检查HTTP状态码 + if (xhr.status === 401 || xhr.status === 403) { + errorMessage = '操作密钥验证失败,请检查密钥是否正确'; + } else if (xhr.responseJSON && xhr.responseJSON.error) { + errorMessage = xhr.responseJSON.error; + } + + showAlertModal('操作失败', errorMessage, 'error'); + console.error('清空信号失败:', error, xhr); + }, + complete: function() { + btn.prop('disabled', false).html(originalText); + } + }); + } + + // 执行立即分析的实际操作 + function executeRunAnalysis(operationKey, stockCount) { + $.ajax({ + url: '/api/run_analysis', + method: 'POST', + contentType: 'application/json', + data: JSON.stringify({ + operation_key: operationKey, + stock_count: stockCount + }), + success: function(response) { + if (response.success) { + // 重置刷新标志 + hasRefreshedAfterCompletion = false; + + // 立即开始状态轮询 + checkAnalysisStatus(); + + // 设置备用刷新机制(10分钟后强制刷新,以防状态检查失败) + setTimeout(() => { + if (!hasRefreshedAfterCompletion) { + console.log('备用刷新机制触发:分析可能已完成但状态检查失败'); + showAlertModal( + '刷新页面', + '分析可能已完成,正在刷新页面显示最新结果', + 'info' + ); + setTimeout(() => location.reload(), 1500); + } + }, 600000); // 10分钟 + + const details = ` +扫描股票: ${response.stock_count} 只
+启动时间: ${response.start_time}
+状态: 页面将自动显示分析进度
+