增加密钥

This commit is contained in:
aaron 2025-10-04 09:26:00 +08:00
parent 9339b7a558
commit 6ec1feb2a7
4 changed files with 927 additions and 66 deletions

View File

@ -14,6 +14,7 @@ services:
- FLASK_ENV=production - FLASK_ENV=production
- PYTHONPATH=/app - PYTHONPATH=/app
- TZ=Asia/Shanghai - TZ=Asia/Shanghai
- OPERATION_KEY=${OPERATION_KEY:-9257}
# MySQL连接配置 (可通过环境变量覆盖) # MySQL连接配置 (可通过环境变量覆盖)
- MYSQL_HOST=${MYSQL_HOST:-cd-cynosdbmysql-grp-7kdd8qe4.sql.tencentcdb.com} - MYSQL_HOST=${MYSQL_HOST:-cd-cynosdbmysql-grp-7kdd8qe4.sql.tencentcdb.com}
- MYSQL_PORT=${MYSQL_PORT:-26558} - MYSQL_PORT=${MYSQL_PORT:-26558}
@ -39,6 +40,7 @@ services:
environment: environment:
- PYTHONPATH=/app - PYTHONPATH=/app
- TZ=Asia/Shanghai - TZ=Asia/Shanghai
- OPERATION_KEY=${OPERATION_KEY:-9257}
- LOG_LEVEL=${LOG_LEVEL:-INFO} - LOG_LEVEL=${LOG_LEVEL:-INFO}
- MARKET_SCAN_STOCKS=${MARKET_SCAN_STOCKS:-200} - MARKET_SCAN_STOCKS=${MARKET_SCAN_STOCKS:-200}
# MySQL连接配置 # MySQL连接配置

View File

@ -5,6 +5,7 @@ AI 智能选股大师 MySQL版本 Web 展示界面
""" """
import sys import sys
import os
from pathlib import Path from pathlib import Path
from flask import Flask, render_template, jsonify, request from flask import Flask, render_template, jsonify, request
from datetime import datetime, date, timedelta from datetime import datetime, date, timedelta
@ -22,6 +23,9 @@ from loguru import logger
app = Flask(__name__) app = Flask(__name__)
app.secret_key = 'trading_ai_mysql_secret_key_2023' app.secret_key = 'trading_ai_mysql_secret_key_2023'
# 操作验证密钥(放在导入后立即定义)
OPERATION_KEY = os.environ.get('OPERATION_KEY', '9257')
# 初始化组件 # 初始化组件
db_manager = MySQLDatabaseManager() db_manager = MySQLDatabaseManager()
config_loader = ConfigLoader() config_loader = ConfigLoader()
@ -172,6 +176,17 @@ def api_stats():
def api_clear_signals(): def api_clear_signals():
"""API接口 - 清空信号数据""" """API接口 - 清空信号数据"""
try: 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)) 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', '') 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 import threading
from datetime import datetime 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']: if analysis_status['is_running']:
return jsonify({ return jsonify({

View File

@ -1110,4 +1110,367 @@ footer .text-muted {
font-size: 0.65rem; font-size: 0.65rem;
padding: 0.2rem 0.4rem; 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;
}
} }

View File

@ -278,6 +278,127 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 自定义模态框 -->
<!-- 密钥输入模态框 -->
<div class="modal fade" id="keyModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title">
<i class="fas fa-key me-2"></i>操作验证
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4">
<div class="text-center mb-3">
<i class="fas fa-shield-alt text-primary" style="font-size: 3rem;"></i>
</div>
<p class="text-center text-muted mb-4">请输入操作验证密钥以继续</p>
<div class="form-group">
<label for="operationKey" class="form-label fw-bold">验证密钥</label>
<input type="password" class="form-control form-control-lg text-center"
id="operationKey" placeholder="请输入验证密钥">
<div class="invalid-feedback" id="keyError"></div>
</div>
</div>
<div class="modal-footer border-0 pt-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times me-1"></i>取消
</button>
<button type="button" class="btn btn-primary" id="confirmKey">
<i class="fas fa-check me-1"></i>确认
</button>
</div>
</div>
</div>
</div>
<!-- 确认操作模态框 -->
<div class="modal fade" id="confirmModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow">
<div class="modal-header" id="confirmHeader">
<h5 class="modal-title" id="confirmTitle">
<i class="fas fa-question-circle me-2"></i>确认操作
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4">
<div class="text-center mb-3">
<i id="confirmIcon" class="fas fa-exclamation-triangle text-warning" style="font-size: 3rem;"></i>
</div>
<p class="text-center" id="confirmMessage">确认要执行此操作吗?</p>
<div id="confirmDetails" class="mt-3" style="display: none;"></div>
</div>
<div class="modal-footer border-0 pt-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times me-1"></i>取消
</button>
<button type="button" class="btn" id="confirmAction">
<i class="fas fa-check me-1"></i>确认
</button>
</div>
</div>
</div>
</div>
<!-- 输入参数模态框 -->
<div class="modal fade" id="inputModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow">
<div class="modal-header bg-info text-white">
<h5 class="modal-title" id="inputTitle">
<i class="fas fa-edit me-2"></i>参数设置
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4">
<div class="form-group">
<label for="inputValue" class="form-label fw-bold" id="inputLabel">请输入参数</label>
<input type="number" class="form-control form-control-lg text-center"
id="inputValue" placeholder="">
<div class="form-text text-muted" id="inputHelp"></div>
<div class="invalid-feedback" id="inputError"></div>
</div>
</div>
<div class="modal-footer border-0 pt-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times me-1"></i>取消
</button>
<button type="button" class="btn btn-info" id="confirmInput">
<i class="fas fa-check me-1"></i>确认
</button>
</div>
</div>
</div>
</div>
<!-- 通知提示模态框 -->
<div class="modal fade" id="alertModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow">
<div class="modal-header" id="alertHeader">
<h5 class="modal-title" id="alertTitle">
<i class="fas fa-info-circle me-2"></i>提示
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4 text-center">
<div class="mb-3">
<i id="alertIcon" class="fas fa-info-circle text-info" style="font-size: 3rem;"></i>
</div>
<p id="alertMessage" class="mb-0">操作完成</p>
<div id="alertDetails" class="mt-3 text-start" style="display: none;"></div>
</div>
<div class="modal-footer border-0 pt-0 justify-content-center">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" id="alertConfirm">
<i class="fas fa-check me-1"></i>确定
</button>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_js %}
@ -298,41 +419,403 @@
window.location.search = urlParams.toString(); window.location.search = urlParams.toString();
} }
// 清空信号按钮 // 全局变量用于存储操作回调
$('#clearSignalsBtn').on('click', function() { let currentOperationCallback = null;
if (confirm('确认要清空最近7天的信号数据吗此操作不可恢复')) { let currentInputCallback = null;
const btn = $(this); let pendingOperationKey = null; // 存储密钥
const originalText = btn.html();
btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin me-1"></i>清空中...'); // ========== 自定义模态框函数 ==========
$.ajax({ // 显示验证密钥模态框
url: '/api/clear_signals', function showKeyModal(title, message, callback) {
method: 'POST', $('#keyModal .modal-title').text(title);
contentType: 'application/json', $('#keyModal p').text(message);
data: JSON.stringify({ $('#operationKey').val('').removeClass('is-invalid');
days: 7, $('#keyError').text('');
strategy_name: '' currentOperationCallback = callback;
}), new bootstrap.Modal($('#keyModal')).show();
success: function(response) { }
if (response.success) {
alert(`清空成功!删除了 ${response.deleted_count} 条信号记录`); // 显示确认操作模态框
location.reload(); function showConfirmModal(title, message, type = 'warning', callback) {
} else { const $header = $('#confirmHeader');
alert('清空失败:' + response.error); const $icon = $('#confirmIcon');
} const $action = $('#confirmAction');
},
error: function(xhr, status, error) { // 设置标题
alert('清空失败:网络错误或服务器异常'); $('#confirmTitle').html(`<i class="fas fa-question-circle me-2"></i>${title}`);
console.error('清空信号失败:', error); $('#confirmMessage').text(message);
},
complete: function() { // 根据类型设置样式
btn.prop('disabled', false).html(originalText); $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(`<i class="fas fa-edit me-2"></i>${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(`<i class="fas fa-info-circle me-2"></i>${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('<i class="fas fa-question-circle me-2"></i>确认清空信号');
$('#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('<i class="fas fa-edit me-2"></i>设置分析参数');
$('#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('<i class="fas fa-question-circle me-2"></i>确认分析');
$('#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('<i class="fas fa-spinner fa-spin me-1"></i>清空中...');
$.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 = `
<div class="text-start">
<p><strong>扫描股票:</strong> ${response.stock_count} 只</p>
<p><strong>启动时间:</strong> ${response.start_time}</p>
<p><strong>状态:</strong> 页面将自动显示分析进度</p>
</div>
`;
showAlertModal(
'分析任务已启动',
'正在后台扫描股票,请稍候...',
'success',
details
);
} 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);
}
});
}
// 全局变量 // 全局变量
let analysisStatusInterval = null; let analysisStatusInterval = null;
@ -408,56 +891,43 @@
</div> </div>
`); `);
// 显示成功完成模态框
showAlertModal(
'分析完成',
'股票扫描分析已完成,页面即将刷新显示最新信号',
'success'
);
setTimeout(() => { setTimeout(() => {
location.reload(); location.reload();
}, 1500); }, 2000);
} }
} }
} }
// 立即分析按钮 // 立即分析按钮
$('#runAnalysisBtn').on('click', function() { $('#runAnalysisBtn').on('click', function() {
const stockCount = prompt('请输入要扫描的股票数量 (默认200):', '200'); // 显示密钥输入模态框
$('#keyModal .modal-title').text('操作验证');
$('#keyModal p').text('请输入操作验证密钥以启动市场分析');
$('#operationKey').val('').removeClass('is-invalid');
$('#keyError').text('');
if (stockCount === null) return; // 用户取消 // 设置全局标识
window.currentOperation = 'run_analysis';
const count = parseInt(stockCount) || 200; new bootstrap.Modal($('#keyModal')).show();
if (count <= 0 || count > 1000) {
alert('股票数量必须在 1-1000 之间');
return;
}
if (confirm(`确认要立即分析 ${count} 只热门股票吗?分析可能需要几分钟时间`)) {
$.ajax({
url: '/api/run_analysis',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({
stock_count: count
}),
success: function(response) {
if (response.success) {
// 重置刷新标志
hasRefreshedAfterCompletion = false;
// 立即开始状态轮询
checkAnalysisStatus();
alert(`分析任务已启动!正在后台扫描 ${response.stock_count} 只股票\n\n启动时间: ${response.start_time}\n\n页面将自动显示分析进度`);
} else {
alert('启动分析失败:' + response.error);
}
},
error: function(xhr, status, error) {
alert('启动分析失败:网络错误或服务器异常');
console.error('启动分析失败:', error);
}
});
}
}); });
// 页面加载时检查是否有正在运行的分析 // 页面加载时检查是否有正在运行的分析
checkAnalysisStatus(); checkAnalysisStatus();
// 页面可见性改变时重新检查分析状态(用户切换回标签页时)
document.addEventListener('visibilitychange', function() {
if (!document.hidden) {
checkAnalysisStatus();
}
});
}); });
</script> </script>
{% endblock %} {% endblock %}