dashboard.html•12.6 kB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>股票数据服务 - 管理面板</title>
    <link rel="stylesheet" href="/static/css/style.css">
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            margin: 0;
            padding: 20px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
        }
        .container {
            max-width: 1200px;
            margin: 0 auto;
            background: white;
            border-radius: 10px;
            padding: 30px;
            box-shadow: 0 10px 30px rgba(0,0,0,0.3);
        }
        .header {
            text-align: center;
            margin-bottom: 40px;
            padding-bottom: 20px;
            border-bottom: 2px solid #f0f0f0;
        }
        .header h1 {
            color: #333;
            margin: 0;
            font-size: 2.5em;
        }
        .header p {
            color: #666;
            margin: 10px 0 0 0;
            font-size: 1.1em;
        }
        .status-panel {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
            gap: 20px;
            margin-bottom: 40px;
        }
        .status-card {
            background: #f8f9fa;
            padding: 20px;
            border-radius: 8px;
            border-left: 4px solid #007bff;
        }
        .status-card.success {
            border-left-color: #28a745;
        }
        .status-card.warning {
            border-left-color: #ffc107;
        }
        .status-card.error {
            border-left-color: #dc3545;
        }
        .status-card h3 {
            margin: 0 0 10px 0;
            color: #333;
        }
        .status-value {
            font-size: 1.5em;
            font-weight: bold;
            color: #007bff;
        }
        .test-panel {
            background: #f8f9fa;
            padding: 30px;
            border-radius: 8px;
            margin-bottom: 30px;
        }
        .test-panel h2 {
            margin: 0 0 20px 0;
            color: #333;
        }
        .input-group {
            margin-bottom: 15px;
        }
        .input-group label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
            color: #555;
        }
        .input-group input, .input-group select {
            width: 100%;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            font-size: 16px;
        }
        .btn {
            background: #007bff;
            color: white;
            border: none;
            padding: 12px 30px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
            margin: 5px;
            transition: background-color 0.3s;
        }
        .btn:hover {
            background: #0056b3;
        }
        .btn.success {
            background: #28a745;
        }
        .btn.success:hover {
            background: #1e7e34;
        }
        .results {
            background: white;
            border: 1px solid #ddd;
            border-radius: 4px;
            padding: 20px;
            margin-top: 20px;
            min-height: 200px;
            max-height: 500px;
            overflow-y: auto;
        }
        .log-entry {
            padding: 8px;
            margin: 2px 0;
            border-radius: 4px;
            font-family: 'Courier New', monospace;
        }
        .log-entry.info {
            background: #d1ecf1;
            color: #0c5460;
        }
        .log-entry.error {
            background: #f8d7da;
            color: #721c24;
        }
        .log-entry.success {
            background: #d4edda;
            color: #155724;
        }
        .loading {
            text-align: center;
            padding: 20px;
            color: #666;
        }
        .spinner {
            border: 4px solid #f3f3f3;
            border-top: 4px solid #3498db;
            border-radius: 50%;
            width: 40px;
            height: 40px;
            animation: spin 2s linear infinite;
            margin: 20px auto;
        }
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>🚀 股票数据服务</h1>
            <p>SSE + HTTP POST 双向通信模式</p>
        </div>
        <div class="status-panel">
            <div class="status-card" id="server-status">
                <h3>📡 服务器状态</h3>
                <div class="status-value" id="server-status-value">检查中...</div>
            </div>
            <div class="status-card" id="sse-status">
                <h3>🔄 SSE 连接</h3>
                <div class="status-value" id="sse-status-value">未连接</div>
            </div>
            <div class="status-card" id="connections-status">
                <h3>👥 活跃连接</h3>
                <div class="status-value" id="connections-count">0</div>
            </div>
        </div>
        <div class="test-panel">
            <h2>📊 股票数据测试</h2>
            <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px;">
                <div class="input-group">
                    <label for="stock-symbol">股票代码:</label>
                    <input type="text" id="stock-symbol" placeholder="例如: 000001, 00700, AAPL" value="000001">
                </div>
                <div class="input-group">
                    <label for="start-date">开始日期:</label>
                    <input type="date" id="start-date">
                </div>
                <div class="input-group">
                    <label for="end-date">结束日期:</label>
                    <input type="date" id="end-date">
                </div>
                <div class="input-group">
                    <label for="test-type">测试类型:</label>
                    <select id="test-type">
                        <option value="price">价格数据</option>
                        <option value="fundamental">基本面分析</option>
                        <option value="news">新闻数据</option>
                    </select>
                </div>
            </div>
            <div style="text-align: center; margin-top: 20px;">
                <button class="btn success" onclick="connectSSE()">🔄 连接 SSE</button>
                <button class="btn" onclick="testAPI()">📊 测试 API</button>
                <button class="btn" onclick="sendMessage()">💬 发送消息</button>
                <button class="btn" onclick="clearResults()">🗑️ 清空结果</button>
            </div>
        </div>
        <div class="results" id="results">
            <div class="loading">
                <div>等待操作...</div>
                <div style="font-size: 0.9em; color: #999; margin-top: 10px;">
                    选择股票代码和测试类型,然后点击相应按钮开始测试
                </div>
            </div>
        </div>
    </div>
    <script src="/static/js/sse-client.js"></script>
    <script src="/static/js/api-client.js"></script>
    <script>
        // 初始化日期
        document.addEventListener('DOMContentLoaded', function() {
            const today = new Date();
            const thirtyDaysAgo = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000);
            
            document.getElementById('end-date').value = today.toISOString().split('T')[0];
            document.getElementById('start-date').value = thirtyDaysAgo.toISOString().split('T')[0];
            
            // 检查服务器状态
            checkServerHealth();
            
            // 定期检查服务器状态
            setInterval(checkServerHealth, 30000);
        });
        async function checkServerHealth() {
            try {
                const response = await fetch('/health');
                const data = await response.json();
                
                document.getElementById('server-status').className = 'status-card success';
                document.getElementById('server-status-value').textContent = data.status;
                document.getElementById('connections-count').textContent = data.connections || 0;
                
                addLogEntry(`✅ 服务器健康检查: ${data.status}`, 'success');
            } catch (error) {
                document.getElementById('server-status').className = 'status-card error';
                document.getElementById('server-status-value').textContent = '离线';
                addLogEntry(`❌ 服务器健康检查失败: ${error.message}`, 'error');
            }
        }
        function addLogEntry(message, type = 'info') {
            const results = document.getElementById('results');
            const entry = document.createElement('div');
            entry.className = `log-entry ${type}`;
            entry.innerHTML = `<strong>${new Date().toLocaleTimeString()}</strong> - ${message}`;
            
            results.appendChild(entry);
            results.scrollTop = results.scrollHeight;
        }
        function clearResults() {
            const results = document.getElementById('results');
            results.innerHTML = '<div class="loading">结果已清空,等待新的操作...</div>';
        }
        async function testAPI() {
            const symbol = document.getElementById('stock-symbol').value;
            const startDate = document.getElementById('start-date').value;
            const endDate = document.getElementById('end-date').value;
            const testType = document.getElementById('test-type').value;
            if (!symbol || !startDate || !endDate) {
                addLogEntry('❌ 请填写完整的测试参数', 'error');
                return;
            }
            addLogEntry(`🚀 开始测试 ${testType} 数据,股票: ${symbol}`, 'info');
            try {
                let endpoint = '';
                const params = new URLSearchParams();
                params.append('symbol', symbol);
                switch (testType) {
                    case 'price':
                        endpoint = '/api/stock/price';
                        params.append('start_date', startDate);
                        params.append('end_date', endDate);
                        break;
                    case 'fundamental':
                        endpoint = '/api/stock/fundamental';
                        break;
                    case 'news':
                        endpoint = '/api/stock/news';
                        params.append('days_back', 30);
                        break;
                }
                const url = `${endpoint}?${params.toString()}`;
                const response = await fetch(url);
                const data = await response.json();
                if (!response.ok) {
                    throw new Error(data.detail || `HTTP ${response.status}`);
                }
                addLogEntry(`✅ API 测试成功`, 'success');
                addLogEntry(`📊 响应数据: ${JSON.stringify(data, null, 2)}`, 'info');
                
            } catch (error) {
                addLogEntry(`❌ API 测试失败: ${error.message}`, 'error');
            }
        }
        async function sendMessage() {
            const symbol = document.getElementById('stock-symbol').value;
            
            if (!symbol) {
                addLogEntry('❌ 请输入股票代码', 'error');
                return;
            }
            const message = {
                type: 'stock_query',
                symbol: symbol,
                timestamp: new Date().toISOString()
            };
            try {
                const response = await fetch('/api/message', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify(message)
                });
                const result = await response.json();
                addLogEntry(`💬 消息发送成功: ${result.message}`, 'success');
                
            } catch (error) {
                addLogEntry(`❌ 消息发送失败: ${error.message}`, 'error');
            }
        }
    </script>
</body>
</html>