947 lines
46 KiB
HTML
947 lines
46 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}交易信号 - AI 智能选股大师{% endblock %}
|
||
|
||
{% block content %}
|
||
<!-- 页面标题和筛选 -->
|
||
<div class="row mb-4">
|
||
<div class="col-12">
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<!-- 操作按钮组 -->
|
||
<div class="d-flex gap-2 align-items-center">
|
||
<button id="clearSignalsBtn" class="btn btn-outline-danger btn-sm">
|
||
<i class="fas fa-trash me-1"></i>清空信号
|
||
</button>
|
||
<button id="runAnalysisBtn" class="btn btn-outline-success btn-sm">
|
||
<i class="fas fa-play me-1"></i>立即分析
|
||
</button>
|
||
|
||
<!-- 分析状态显示 -->
|
||
<div id="analysisStatus" class="ms-3" style="display: none;">
|
||
<div class="d-flex align-items-center">
|
||
<div class="spinner-border spinner-border-sm text-primary me-2" role="status">
|
||
<span class="visually-hidden">分析中...</span>
|
||
</div>
|
||
<div class="analysis-status-text">
|
||
<small class="text-muted fw-bold">正在分析中...</small>
|
||
<div class="mt-1">
|
||
<div class="progress" style="width: 150px; height: 6px;">
|
||
<div id="analysisProgress" class="progress-bar bg-success" role="progressbar" style="width: 0%"></div>
|
||
</div>
|
||
<small id="analysisInfo" class="text-muted">启动中...</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 筛选表单 -->
|
||
<form method="GET" class="d-flex gap-2 align-items-center">
|
||
<select name="asset_type" class="form-select form-select-sm">
|
||
<option value="">所有资产</option>
|
||
<option value="stock" {% if asset_type == 'stock' %}selected{% endif %}>股票</option>
|
||
<option value="crypto" {% if asset_type == 'crypto' %}selected{% endif %}>加密货币</option>
|
||
</select>
|
||
|
||
<select name="strategy" class="form-select form-select-sm">
|
||
<option value="">所有策略</option>
|
||
<option value="K线形态策略" {% if strategy_name == 'K线形态策略' %}selected{% endif %}>K线形态策略</option>
|
||
</select>
|
||
|
||
<select name="timeframe" class="form-select form-select-sm">
|
||
<option value="">所有周期</option>
|
||
<option value="daily" {% if timeframe == 'daily' or not timeframe %}selected{% endif %}>日线</option>
|
||
<option value="weekly" {% if timeframe == 'weekly' %}selected{% endif %}>周线</option>
|
||
</select>
|
||
|
||
<select name="days" class="form-select form-select-sm">
|
||
<option value="7" {% if days == 7 %}selected{% endif %}>最近7天</option>
|
||
<option value="15" {% if days == 15 %}selected{% endif %}>最近15天</option>
|
||
<option value="30" {% if days == 30 %}selected{% endif %}>最近30天</option>
|
||
<option value="90" {% if days == 90 %}selected{% endif %}>最近90天</option>
|
||
</select>
|
||
|
||
<select name="per_page" class="form-select form-select-sm">
|
||
<option value="20" {% if per_page == 20 %}selected{% endif %}>每页20条</option>
|
||
<option value="50" {% if per_page == 50 %}selected{% endif %}>每页50条</option>
|
||
<option value="100" {% if per_page == 100 %}selected{% endif %}>每页100条</option>
|
||
</select>
|
||
|
||
<button type="submit" class="btn btn-primary btn-sm text-nowrap">
|
||
<i class="fas fa-filter me-1"></i>筛选
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<!-- 信号表格 -->
|
||
<div class="row">
|
||
<div class="col-12">
|
||
<div class="card fade-in">
|
||
<div class="card-header">
|
||
<h5 class="mb-0 text-primary fw-bold">
|
||
<i class="fas fa-table me-2"></i>详细信号数据
|
||
</h5>
|
||
</div>
|
||
<div class="card-body p-0">
|
||
{% if signals_grouped %}
|
||
<div class="table-container">
|
||
{% for scan_hour, signals in signals_grouped.items() %}
|
||
<!-- 扫描时间分组标题 -->
|
||
<div class="scan-date-header bg-light px-3 py-2">
|
||
<h6 class="mb-0 text-primary fw-bold">
|
||
<i class="fas fa-clock me-2"></i>扫描时间: {{ scan_hour.strftime('%Y-%m-%d %H:%M') }}
|
||
<span class="badge bg-primary ms-2">{{ signals|length }} 条信号</span>
|
||
</h6>
|
||
</div>
|
||
|
||
<!-- 该扫描时间下的信号表格 -->
|
||
<div class="table-responsive">
|
||
<table class="table table-hover mb-0">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th style="width: 250px;">股票</th>
|
||
<th style="width: 150px;">策略</th>
|
||
<th style="width: 100px;">周期</th>
|
||
<th style="width: 500px;">时间线流程</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for signal in signals %}
|
||
<tr>
|
||
<td>
|
||
<div class="d-flex align-items-center">
|
||
{% if signal.asset_type == 'crypto' %}
|
||
{# 加密货币链接到 Binance #}
|
||
<a href="https://www.binance.com/zh-CN/trade/{{ signal.stock_code }}" target="_blank" class="stock-code-link me-2">
|
||
<div class="stock-code-badge bg-warning text-dark">{{ signal.stock_code }}</div>
|
||
</a>
|
||
{% else %}
|
||
{# 股票链接到雪球 #}
|
||
<a href="https://xueqiu.com/S/{% if signal.stock_code.endswith('.SZ') %}SZ{% elif signal.stock_code.endswith('.SH') %}SH{% elif signal.stock_code.startswith('0') or signal.stock_code.startswith('3') %}SZ{% else %}SH{% endif %}{{ signal.stock_code.split('.')[0] }}" target="_blank" class="stock-code-link me-2">
|
||
<div class="stock-code-badge">{{ signal.stock_code }}</div>
|
||
</a>
|
||
{% endif %}
|
||
<div class="stock-name text-truncate">{{ signal.stock_name or '未知' }}</div>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<span class="badge bg-primary">{{ signal.strategy_name }}</span>
|
||
{% if signal.new_high_confirmed %}
|
||
<div class="mt-1">
|
||
<small class="badge bg-success">创新高回踩确认</small>
|
||
</div>
|
||
{% endif %}
|
||
</td>
|
||
<td>
|
||
<span class="badge bg-{% if signal.timeframe == 'daily' %}primary{% else %}success{% endif %}">
|
||
{{ '日线' if signal.timeframe == 'daily' else '周线' }}
|
||
</span>
|
||
</td>
|
||
<td style="min-width: 500px; padding: 12px 20px;">
|
||
{% if signal.new_high_confirmed %}
|
||
<!-- 新格式:优化的创新高回踩确认时间线 -->
|
||
<div class="timeline-enhanced-wide">
|
||
<div class="timeline-flow">
|
||
<div class="timeline-step-wide">
|
||
<div class="timeline-marker-wide pattern-marker">
|
||
<i class="fas fa-chart-line"></i>
|
||
</div>
|
||
<div class="timeline-content-wide">
|
||
<div class="timeline-title">📅 模式识别</div>
|
||
<div class="timeline-date-wide">{{ signal.signal_date | datetime_format('%m-%d') }}</div>
|
||
<div class="timeline-info">突破价: {{ signal.breakout_price | currency }}元</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="timeline-arrow">
|
||
<i class="fas fa-arrow-right"></i>
|
||
</div>
|
||
|
||
{% if signal.new_high_date %}
|
||
<div class="timeline-step-wide">
|
||
<div class="timeline-marker-wide new-high-marker">
|
||
<i class="fas fa-rocket"></i>
|
||
</div>
|
||
<div class="timeline-content-wide">
|
||
<div class="timeline-title">🚀 创新高</div>
|
||
<div class="timeline-date-wide highlight">{{ signal.new_high_date | datetime_format('%m-%d') }}</div>
|
||
{% if signal.new_high_price %}
|
||
<div class="timeline-info">{{ signal.new_high_price | currency }}元</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="timeline-arrow">
|
||
<i class="fas fa-arrow-right"></i>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if signal.confirmation_date %}
|
||
<div class="timeline-step-wide">
|
||
<div class="timeline-marker-wide confirmation-marker">
|
||
<i class="fas fa-check-circle"></i>
|
||
</div>
|
||
<div class="timeline-content-wide">
|
||
<div class="timeline-title">✅ 回踩确认</div>
|
||
<div class="timeline-date-wide highlight">{{ signal.confirmation_date | datetime_format('%m-%d') }}</div>
|
||
{% if signal.confirmation_days %}
|
||
<div class="timeline-info">用时{{ signal.confirmation_days }}天</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- 技术指标概要 -->
|
||
<div class="timeline-summary mt-2">
|
||
<span class="badge bg-{% if signal.breakout_pct and signal.breakout_pct > 3 %}success{% elif signal.breakout_pct and signal.breakout_pct > 1 %}warning{% else %}secondary{% endif %} me-1 badge-sm">
|
||
突破{{ signal.breakout_pct | percentage }}
|
||
</span>
|
||
<span class="badge bg-{% if signal.final_yang_entity_ratio and signal.final_yang_entity_ratio > 0.6 %}success{% elif signal.final_yang_entity_ratio and signal.final_yang_entity_ratio > 0.4 %}warning{% else %}secondary{% endif %} me-1 badge-sm">
|
||
实体{{ signal.final_yang_entity_ratio | percentage }}
|
||
</span>
|
||
<span class="badge bg-{% if signal.above_ema20 %}success{% else %}secondary{% endif %} me-1 badge-sm">
|
||
EMA20{{ '✅' if signal.above_ema20 else '❌' }}
|
||
</span>
|
||
{% if signal.pullback_distance %}
|
||
<span class="badge bg-{% if signal.pullback_distance > -2 %}success{% elif signal.pullback_distance > -5 %}warning{% else %}danger{% endif %} me-1 badge-sm">
|
||
回踩{{ signal.pullback_distance | currency }}%
|
||
</span>
|
||
{% endif %}
|
||
<span class="badge bg-light text-muted me-1 badge-sm">
|
||
{{ signal.scan_time | datetime_format('%m-%d %H:%M') }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
{% else %}
|
||
<!-- 旧格式:兼容显示 -->
|
||
<div class="timeline-simple">
|
||
<div class="text-muted">{{ signal.signal_date | datetime_format('%Y-%m-%d') }}</div>
|
||
<div class="text-success fw-bold">{{ signal.breakout_price | currency }}元</div>
|
||
</div>
|
||
{% endif %}
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
|
||
<!-- 分页 -->
|
||
{% if total_pages > 1 %}
|
||
<nav aria-label="信号分页" class="mt-4">
|
||
<ul class="pagination justify-content-center">
|
||
<!-- 上一页 -->
|
||
<li class="page-item {% if not has_prev %}disabled{% endif %}">
|
||
<a class="page-link" href="{% if has_prev %}{{ url_for('signals', page=current_page-1, strategy=strategy_name, asset_type=asset_type, timeframe=timeframe, days=days, per_page=per_page) }}{% else %}#{% endif %}">
|
||
<i class="fas fa-chevron-left"></i> 上一页
|
||
</a>
|
||
</li>
|
||
|
||
<!-- 页码 -->
|
||
{% set start_page = [1, current_page - 2]|max %}
|
||
{% set end_page = [total_pages, current_page + 2]|min %}
|
||
|
||
{% if start_page > 1 %}
|
||
<li class="page-item">
|
||
<a class="page-link" href="{{ url_for('signals', page=1, strategy=strategy_name, asset_type=asset_type, timeframe=timeframe, days=days, per_page=per_page) }}">1</a>
|
||
</li>
|
||
{% if start_page > 2 %}
|
||
<li class="page-item disabled"><span class="page-link">...</span></li>
|
||
{% endif %}
|
||
{% endif %}
|
||
|
||
{% for page_num in range(start_page, end_page + 1) %}
|
||
<li class="page-item {% if page_num == current_page %}active{% endif %}">
|
||
<a class="page-link" href="{{ url_for('signals', page=page_num, strategy=strategy_name, asset_type=asset_type, timeframe=timeframe, days=days, per_page=per_page) }}">{{ page_num }}</a>
|
||
</li>
|
||
{% endfor %}
|
||
|
||
{% if end_page < total_pages %}
|
||
{% if end_page < total_pages - 1 %}
|
||
<li class="page-item disabled"><span class="page-link">...</span></li>
|
||
{% endif %}
|
||
<li class="page-item">
|
||
<a class="page-link" href="{{ url_for('signals', page=total_pages, strategy=strategy_name, asset_type=asset_type, timeframe=timeframe, days=days, per_page=per_page) }}">{{ total_pages }}</a>
|
||
</li>
|
||
{% endif %}
|
||
|
||
<!-- 下一页 -->
|
||
<li class="page-item {% if not has_next %}disabled{% endif %}">
|
||
<a class="page-link" href="{% if has_next %}{{ url_for('signals', page=current_page+1, strategy=strategy_name, asset_type=asset_type, timeframe=timeframe, days=days, per_page=per_page) }}{% else %}#{% endif %}">
|
||
下一页 <i class="fas fa-chevron-right"></i>
|
||
</a>
|
||
</li>
|
||
</ul>
|
||
</nav>
|
||
{% endif %}
|
||
|
||
{% else %}
|
||
<div class="empty-state">
|
||
<i class="fas fa-signal"></i>
|
||
<h4>暂无信号数据</h4>
|
||
<p>请尝试调整筛选条件或稍后再试</p>
|
||
</div>
|
||
{% endif %}
|
||
</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 %}
|
||
|
||
{% block extra_js %}
|
||
<script>
|
||
$(document).ready(function() {
|
||
// 表格行点击高亮
|
||
$('tbody tr').on('click', function() {
|
||
$(this).toggleClass('table-active');
|
||
});
|
||
|
||
// 工具提示
|
||
$('[data-bs-toggle="tooltip"]').tooltip();
|
||
|
||
// 如果没有timeframe参数,自动设置为daily并重新加载
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
if (!urlParams.has('timeframe')) {
|
||
urlParams.set('timeframe', 'daily');
|
||
window.location.search = urlParams.toString();
|
||
}
|
||
|
||
// 全局变量用于存储操作回调
|
||
let currentOperationCallback = null;
|
||
let currentInputCallback = null;
|
||
let pendingOperationKey = null; // 存储密钥
|
||
|
||
// ========== 自定义模态框函数 ==========
|
||
|
||
// 显示验证密钥模态框
|
||
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(`<i class="fas fa-question-circle me-2"></i>${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(`<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;
|
||
|
||
// 检查分析状态
|
||
function checkAnalysisStatus() {
|
||
$.ajax({
|
||
url: '/api/analysis_status',
|
||
method: 'GET',
|
||
success: function(response) {
|
||
if (response.success && response.data) {
|
||
updateAnalysisStatusUI(response.data);
|
||
}
|
||
},
|
||
error: function(xhr, status, error) {
|
||
console.error('获取分析状态失败:', error);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 用于防止重复刷新的标志
|
||
let hasRefreshedAfterCompletion = false;
|
||
|
||
// 更新分析状态UI
|
||
function updateAnalysisStatusUI(status) {
|
||
const $statusDiv = $('#analysisStatus');
|
||
const $progress = $('#analysisProgress');
|
||
const $info = $('#analysisInfo');
|
||
const $runBtn = $('#runAnalysisBtn');
|
||
|
||
if (status.is_running) {
|
||
// 显示分析状态
|
||
$statusDiv.show();
|
||
$runBtn.prop('disabled', true).html('<i class="fas fa-cog fa-spin me-1"></i>分析中...');
|
||
|
||
// 更新进度条
|
||
$progress.css('width', status.progress + '%');
|
||
|
||
// 更新信息文本
|
||
const runningTime = Math.floor(status.running_time / 60);
|
||
const remainingTime = Math.max(0, Math.ceil((status.stock_count / 200 * 5) - (status.running_time / 60)));
|
||
|
||
if (status.progress >= 95) {
|
||
$info.text(`分析即将完成... ${status.progress}%`);
|
||
} else {
|
||
$info.text(`进度: ${status.progress}% | 已运行: ${runningTime}分钟 | 预计剩余: ${remainingTime}分钟`);
|
||
}
|
||
|
||
// 开始轮询(如果还没开始)
|
||
if (!analysisStatusInterval) {
|
||
analysisStatusInterval = setInterval(checkAnalysisStatus, 3000); // 每3秒检查一次
|
||
}
|
||
} else {
|
||
// 分析已完成,隐藏分析状态
|
||
$statusDiv.hide();
|
||
$runBtn.prop('disabled', false).html('<i class="fas fa-play me-1"></i>立即分析');
|
||
|
||
// 停止轮询
|
||
if (analysisStatusInterval) {
|
||
clearInterval(analysisStatusInterval);
|
||
analysisStatusInterval = null;
|
||
}
|
||
|
||
// 如果是刚完成分析且还没刷新过,则刷新页面
|
||
if (status.progress >= 95 && !hasRefreshedAfterCompletion) {
|
||
hasRefreshedAfterCompletion = true;
|
||
|
||
// 显示完成提示
|
||
$statusDiv.show();
|
||
$statusDiv.html(`
|
||
<div class="d-flex align-items-center">
|
||
<i class="fas fa-check-circle text-success me-2"></i>
|
||
<small class="text-success fw-bold">分析完成!正在加载新结果...</small>
|
||
</div>
|
||
`);
|
||
|
||
// 显示成功完成模态框
|
||
showAlertModal(
|
||
'分析完成',
|
||
'股票扫描分析已完成,页面即将刷新显示最新信号',
|
||
'success'
|
||
);
|
||
|
||
setTimeout(() => {
|
||
location.reload();
|
||
}, 2000);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 立即分析按钮
|
||
$('#runAnalysisBtn').on('click', function() {
|
||
// 显示密钥输入模态框
|
||
$('#keyModal .modal-title').text('操作验证');
|
||
$('#keyModal p').text('请输入操作验证密钥以启动市场分析');
|
||
$('#operationKey').val('').removeClass('is-invalid');
|
||
$('#keyError').text('');
|
||
|
||
// 设置全局标识
|
||
window.currentOperation = 'run_analysis';
|
||
|
||
new bootstrap.Modal($('#keyModal')).show();
|
||
});
|
||
|
||
// 页面加载时检查是否有正在运行的分析
|
||
checkAnalysisStatus();
|
||
|
||
// 页面可见性改变时重新检查分析状态(用户切换回标签页时)
|
||
document.addEventListener('visibilitychange', function() {
|
||
if (!document.hidden) {
|
||
checkAnalysisStatus();
|
||
}
|
||
});
|
||
});
|
||
</script>
|
||
{% endblock %} |