commit 7a290ef4f71293650bc4294e7332d5c75c1a11ed Author: hz4th_coder Date: Sun Jun 28 15:57:43 2026 +0800 世界杯预测系统 v1.0.0 - 初始版本 diff --git a/README.md b/README.md new file mode 100644 index 0000000..937cf0e --- /dev/null +++ b/README.md @@ -0,0 +1,132 @@ +# 世界杯预测系统 + +一个展示世界杯比赛预测结果的Web应用,支持多算法预测对比和管理后台。 + +## 功能特点 + +### 前端展示 +- 📊 **统计概览**:总比赛数、即将进行的比赛、算法数量、总体准确率 +- 📅 **即将进行的比赛**:展示未来比赛及其多个算法的预测结果 +- ✅ **已完成的比赛**:展示历史比赛结果和预测准确情况 +- 🤖 **算法排行榜**:展示各算法的准确率、预测数、正确数统计 +- 🎯 **预测详情**:点击比赛或算法可查看详细预测历史 + +### 管理后台 +- ⚙️ **系统配置**: + - 预测周期(小时) + - 最近预测时间 + - 世界杯年份 +- 🤖 **算法管理**: + - 添加/编辑/删除预测算法 + - 启用/禁用算法状态 +- ⚽ **比赛管理**: + - 添加/编辑/删除比赛 + - 设置比赛时间、比分、状态 +- 🎯 **预测管理**: + - 添加预测结果 + - 评估预测准确性 + - 按比赛筛选预测 + +## 技术栈 + +- **后端**:Python Flask +- **数据库**:SQLite +- **前端**:HTML/CSS/JavaScript(原生,无框架) +- **样式**:现代玻璃态设计(Glassmorphism) + +## 访问地址 + +系统已部署在端口 **16015**: + +- **主页**:http://192.168.0.101:16015/ +- **管理后台**:http://192.168.0.101:16015/admin + +## 快速启动 + +```bash +# 进入项目目录 +cd /home/openclaw/.openclaw/workspace-hz4th_coder/works/worldcup-predictor + +# 启动服务 +/home/hz1/miniconda3/envs/openclaw/bin/python backend/app.py +``` + +## API接口 + +### 比赛 +- `GET /api/matches` - 获取所有比赛 +- `GET /api/matches?status=upcoming` - 获取即将进行的比赛 +- `GET /api/matches?status=finished` - 获取已完成的比赛 +- `GET /api/matches/{id}` - 获取单场比赛详情 +- `POST /api/matches` - 添加比赛 +- `PUT /api/matches/{id}` - 更新比赛 +- `DELETE /api/matches/{id}` - 删除比赛 + +### 算法 +- `GET /api/algorithms` - 获取所有算法 +- `GET /api/algorithms/{id}` - 获取单个算法详情(含预测历史) +- `POST /api/algorithms` - 添加算法 +- `PUT /api/algorithms/{id}` - 更新算法 +- `DELETE /api/algorithms/{id}` - 删除算法 + +### 预测 +- `POST /api/predictions` - 添加预测 +- `POST /api/predictions/evaluate` - 评估已完成比赛的预测结果 + +### 配置与统计 +- `GET /api/config` - 获取系统配置 +- `POST /api/config` - 更新系统配置 +- `GET /api/stats/overview` - 获取总体统计 + +## 数据库结构 + +### algorithms(算法表) +- id, name, description, accuracy, total_predictions, correct_predictions, is_active + +### matches(比赛表) +- id, match_code, home_team, away_team, match_time, home_score, away_score, status, round_name + +### predictions(预测表) +- id, match_id, algorithm_id, predicted_home_score, predicted_away_score, predicted_winner, confidence, is_correct + +### system_config(配置表) +- config_key, config_value, updated_at + +## 示例数据 + +系统已预置示例数据: +- 4个预测算法(AI预测模型、专家预测系统、情绪分析模型、历史数据模型) +- 5场比赛(决赛、半决赛、8强、小组赛) +- 10条预测记录 + +## 文件结构 + +``` +worldcup-predictor/ +├── backend/ +│ ├── app.py # Flask后端应用 +│ ├── requirements.txt # Python依赖 +│ └── worldcup.db # SQLite数据库 +├── frontend/ +│ ├── index.html # 主页 +│ ├── admin.html # 管理后台 +│ ├── css/ +│ │ └── style.css # 样式文件 +│ └── js/ +│ ├── app.js # 主页逻辑 +│ └── admin.js # 管理后台逻辑 +├── start.sh # 启动脚本 +└── README.md # 说明文档 +``` + +## 注意事项 + +- 当前使用Flask开发服务器,适合演示和测试 +- 生产环境建议使用nginx + gunicorn部署 +- 数据库文件位于 `backend/worldcup.db`,可直接备份 +- 支持跨域访问(CORS已启用) + +--- + +开发时间:2026-06-28 +开发者:黄庄4号程序员 \ No newline at end of file diff --git a/backend/app.py b/backend/app.py new file mode 100644 index 0000000..1ff1803 --- /dev/null +++ b/backend/app.py @@ -0,0 +1,576 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +世界杯预测系统 - 后端API +端口: 16015 +""" + +from flask import Flask, jsonify, request, send_from_directory +from flask_cors import CORS +import sqlite3 +import json +from datetime import datetime, timedelta +import os + +app = Flask(__name__, static_folder='../frontend') +CORS(app) + +DB_PATH = os.path.join(os.path.dirname(__file__), 'worldcup.db') + + +def get_db(): + """获取数据库连接""" + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + return conn + + +def init_db(): + """初始化数据库""" + conn = get_db() + cursor = conn.cursor() + + # 预测算法表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS algorithms ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + description TEXT, + accuracy REAL DEFAULT 0, + total_predictions INTEGER DEFAULT 0, + correct_predictions INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + is_active INTEGER DEFAULT 1 + ) + ''') + + # 比赛表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS matches ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + match_code TEXT NOT NULL UNIQUE, + home_team TEXT NOT NULL, + away_team TEXT NOT NULL, + match_time TIMESTAMP, + home_score INTEGER, + away_score INTEGER, + status TEXT DEFAULT 'upcoming', + round_name TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # 预测结果表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS predictions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + match_id INTEGER NOT NULL, + algorithm_id INTEGER NOT NULL, + predicted_home_score INTEGER, + predicted_away_score INTEGER, + predicted_winner TEXT, + confidence REAL, + predicted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + is_correct INTEGER, + FOREIGN KEY (match_id) REFERENCES matches(id), + FOREIGN KEY (algorithm_id) REFERENCES algorithms(id), + UNIQUE(match_id, algorithm_id) + ) + ''') + + # 系统配置表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS system_config ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + config_key TEXT NOT NULL UNIQUE, + config_value TEXT, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # 插入默认配置 + default_configs = [ + ('prediction_cycle_hours', '24'), + ('last_prediction_time', datetime.now().isoformat()), + ('world_cup_year', '2026') + ] + + for key, value in default_configs: + cursor.execute(''' + INSERT OR IGNORE INTO system_config (config_key, config_value) + VALUES (?, ?) + ''', (key, value)) + + conn.commit() + conn.close() + + +def calculate_algorithm_stats(algorithm_id): + """计算算法统计数据""" + conn = get_db() + cursor = conn.cursor() + + cursor.execute(''' + SELECT + COUNT(*) as total, + SUM(CASE WHEN is_correct = 1 THEN 1 ELSE 0 END) as correct + FROM predictions + WHERE algorithm_id = ? AND is_correct IS NOT NULL + ''', (algorithm_id,)) + + result = cursor.fetchone() + total = result['total'] or 0 + correct = result['correct'] or 0 + accuracy = (correct / total * 100) if total > 0 else 0 + + cursor.execute(''' + UPDATE algorithms + SET total_predictions = ?, correct_predictions = ?, accuracy = ? + WHERE id = ? + ''', (total, correct, accuracy, algorithm_id)) + + conn.commit() + conn.close() + + +# ==================== 前端页面路由 ==================== + +@app.route('/') +def index(): + """主页""" + return send_from_directory(app.static_folder, 'index.html') + + +@app.route('/admin') +def admin(): + """管理后台""" + return send_from_directory(app.static_folder, 'admin.html') + + +@app.route('/css/') +def css(filename): + return send_from_directory(os.path.join(app.static_folder, 'css'), filename) + + +@app.route('/js/') +def js(filename): + return send_from_directory(os.path.join(app.static_folder, 'js'), filename) + + +# ==================== API路由 ==================== + +@app.route('/api/matches', methods=['GET']) +def get_matches(): + """获取所有比赛""" + status = request.args.get('status', 'all') + + conn = get_db() + cursor = conn.cursor() + + if status == 'upcoming': + cursor.execute(''' + SELECT * FROM matches + WHERE status = 'upcoming' OR status IS NULL + ORDER BY match_time ASC + ''') + elif status == 'finished': + cursor.execute(''' + SELECT * FROM matches + WHERE status = 'finished' + ORDER BY match_time DESC + ''') + else: + cursor.execute('SELECT * FROM matches ORDER BY match_time DESC') + + matches = [dict(row) for row in cursor.fetchall()] + conn.close() + + return jsonify({'success': True, 'data': matches}) + + +@app.route('/api/matches/', methods=['GET']) +def get_match(match_id): + """获取单场比赛详情""" + conn = get_db() + cursor = conn.cursor() + + cursor.execute('SELECT * FROM matches WHERE id = ?', (match_id,)) + match = cursor.fetchone() + + if not match: + conn.close() + return jsonify({'success': False, 'message': '比赛不存在'}), 404 + + # 获取该比赛的所有预测 + cursor.execute(''' + SELECT p.*, a.name as algorithm_name, a.accuracy as algorithm_accuracy + FROM predictions p + JOIN algorithms a ON p.algorithm_id = a.id + WHERE p.match_id = ? + ''', (match_id,)) + + predictions = [dict(row) for row in cursor.fetchall()] + conn.close() + + return jsonify({ + 'success': True, + 'data': { + 'match': dict(match), + 'predictions': predictions + } + }) + + +@app.route('/api/matches', methods=['POST']) +def add_match(): + """添加比赛""" + data = request.json + + conn = get_db() + cursor = conn.cursor() + + try: + cursor.execute(''' + INSERT INTO matches (match_code, home_team, away_team, match_time, round_name, status) + VALUES (?, ?, ?, ?, ?, ?) + ''', ( + data['match_code'], + data['home_team'], + data['away_team'], + data.get('match_time'), + data.get('round_name', '小组赛'), + data.get('status', 'upcoming') + )) + + match_id = cursor.lastrowid + conn.commit() + conn.close() + + return jsonify({'success': True, 'data': {'id': match_id}}) + except sqlite3.IntegrityError: + conn.close() + return jsonify({'success': False, 'message': '比赛代码已存在'}), 400 + + +@app.route('/api/matches/', methods=['PUT']) +def update_match(match_id): + """更新比赛""" + data = request.json + + conn = get_db() + cursor = conn.cursor() + + cursor.execute('SELECT id FROM matches WHERE id = ?', (match_id,)) + if not cursor.fetchone(): + conn.close() + return jsonify({'success': False, 'message': '比赛不存在'}), 404 + + update_fields = [] + values = [] + + for field in ['home_team', 'away_team', 'match_time', 'home_score', 'away_score', 'status', 'round_name']: + if field in data: + update_fields.append(f'{field} = ?') + values.append(data[field]) + + if update_fields: + values.append(match_id) + cursor.execute(f''' + UPDATE matches SET {', '.join(update_fields)} + WHERE id = ? + ''', values) + conn.commit() + + conn.close() + return jsonify({'success': True}) + + +@app.route('/api/matches/', methods=['DELETE']) +def delete_match(match_id): + """删除比赛""" + conn = get_db() + cursor = conn.cursor() + + cursor.execute('DELETE FROM predictions WHERE match_id = ?', (match_id,)) + cursor.execute('DELETE FROM matches WHERE id = ?', (match_id,)) + conn.commit() + conn.close() + + return jsonify({'success': True}) + + +@app.route('/api/algorithms', methods=['GET']) +def get_algorithms(): + """获取所有算法""" + conn = get_db() + cursor = conn.cursor() + + cursor.execute('SELECT * FROM algorithms ORDER BY accuracy DESC') + algorithms = [dict(row) for row in cursor.fetchall()] + conn.close() + + return jsonify({'success': True, 'data': algorithms}) + + +@app.route('/api/algorithms/', methods=['GET']) +def get_algorithm(algorithm_id): + """获取单个算法详情""" + conn = get_db() + cursor = conn.cursor() + + cursor.execute('SELECT * FROM algorithms WHERE id = ?', (algorithm_id,)) + algorithm = cursor.fetchone() + + if not algorithm: + conn.close() + return jsonify({'success': False, 'message': '算法不存在'}), 404 + + # 获取该算法的预测历史 + cursor.execute(''' + SELECT p.*, m.home_team, m.away_team, m.home_score, m.away_score, m.match_time + FROM predictions p + JOIN matches m ON p.match_id = m.id + WHERE p.algorithm_id = ? + ORDER BY m.match_time DESC + LIMIT 50 + ''', (algorithm_id,)) + + history = [dict(row) for row in cursor.fetchall()] + conn.close() + + return jsonify({ + 'success': True, + 'data': { + 'algorithm': dict(algorithm), + 'history': history + } + }) + + +@app.route('/api/algorithms', methods=['POST']) +def add_algorithm(): + """添加算法""" + data = request.json + + conn = get_db() + cursor = conn.cursor() + + try: + cursor.execute(''' + INSERT INTO algorithms (name, description) + VALUES (?, ?) + ''', (data['name'], data.get('description', ''))) + + algorithm_id = cursor.lastrowid + conn.commit() + conn.close() + + return jsonify({'success': True, 'data': {'id': algorithm_id}}) + except sqlite3.IntegrityError: + conn.close() + return jsonify({'success': False, 'message': '算法名称已存在'}), 400 + + +@app.route('/api/algorithms/', methods=['PUT']) +def update_algorithm(algorithm_id): + """更新算法""" + data = request.json + + conn = get_db() + cursor = conn.cursor() + + cursor.execute('SELECT id FROM algorithms WHERE id = ?', (algorithm_id,)) + if not cursor.fetchone(): + conn.close() + return jsonify({'success': False, 'message': '算法不存在'}), 404 + + update_fields = [] + values = [] + + for field in ['name', 'description', 'is_active']: + if field in data: + update_fields.append(f'{field} = ?') + values.append(data[field]) + + if update_fields: + values.append(algorithm_id) + cursor.execute(f''' + UPDATE algorithms SET {', '.join(update_fields)} + WHERE id = ? + ''', values) + conn.commit() + + conn.close() + return jsonify({'success': True}) + + +@app.route('/api/algorithms/', methods=['DELETE']) +def delete_algorithm(algorithm_id): + """删除算法""" + conn = get_db() + cursor = conn.cursor() + + cursor.execute('DELETE FROM predictions WHERE algorithm_id = ?', (algorithm_id,)) + cursor.execute('DELETE FROM algorithms WHERE id = ?', (algorithm_id,)) + conn.commit() + conn.close() + + return jsonify({'success': True}) + + +@app.route('/api/predictions', methods=['POST']) +def add_prediction(): + """添加预测""" + data = request.json + + conn = get_db() + cursor = conn.cursor() + + try: + cursor.execute(''' + INSERT OR REPLACE INTO predictions + (match_id, algorithm_id, predicted_home_score, predicted_away_score, predicted_winner, confidence) + VALUES (?, ?, ?, ?, ?, ?) + ''', ( + data['match_id'], + data['algorithm_id'], + data.get('predicted_home_score'), + data.get('predicted_away_score'), + data.get('predicted_winner'), + data.get('confidence', 0.5) + )) + + conn.commit() + conn.close() + + return jsonify({'success': True}) + except Exception as e: + conn.close() + return jsonify({'success': False, 'message': str(e)}), 400 + + +@app.route('/api/predictions/evaluate', methods=['POST']) +def evaluate_predictions(): + """评估预测结果""" + conn = get_db() + cursor = conn.cursor() + + # 获取所有已完成的比赛 + cursor.execute(''' + SELECT m.*, p.id as prediction_id, p.predicted_home_score, p.predicted_away_score, + p.algorithm_id, p.predicted_winner + FROM matches m + JOIN predictions p ON m.id = p.match_id + WHERE m.status = 'finished' AND m.home_score IS NOT NULL AND m.away_score IS NOT NULL + AND p.is_correct IS NULL + ''') + + predictions = cursor.fetchall() + + for pred in predictions: + # 判断预测是否正确 + is_correct = 0 + + if pred['predicted_home_score'] and pred['predicted_away_score']: + # 比分预测 + if pred['predicted_home_score'] == pred['home_score'] and pred['predicted_away_score'] == pred['away_score']: + is_correct = 1 + elif pred['predicted_winner']: + # 胜负预测 + actual_winner = 'draw' if pred['home_score'] == pred['away_score'] else ( + pred['home_team'] if pred['home_score'] > pred['away_score'] else pred['away_team'] + ) + if pred['predicted_winner'] == actual_winner: + is_correct = 1 + + cursor.execute(''' + UPDATE predictions SET is_correct = ? WHERE id = ? + ''', (is_correct, pred['prediction_id'])) + + # 更新算法统计 + calculate_algorithm_stats(pred['algorithm_id']) + + conn.commit() + conn.close() + + return jsonify({'success': True, 'evaluated': len(predictions)}) + + +@app.route('/api/config', methods=['GET']) +def get_config(): + """获取系统配置""" + conn = get_db() + cursor = conn.cursor() + + cursor.execute('SELECT config_key, config_value FROM system_config') + configs = {row['config_key']: row['config_value'] for row in cursor.fetchall()} + conn.close() + + return jsonify({'success': True, 'data': configs}) + + +@app.route('/api/config', methods=['POST']) +def update_config(): + """更新系统配置""" + data = request.json + + conn = get_db() + cursor = conn.cursor() + + for key, value in data.items(): + cursor.execute(''' + INSERT OR REPLACE INTO system_config (config_key, config_value, updated_at) + VALUES (?, ?, ?) + ''', (key, str(value), datetime.now().isoformat())) + + conn.commit() + conn.close() + + return jsonify({'success': True}) + + +@app.route('/api/stats/overview', methods=['GET']) +def get_overview_stats(): + """获取总体统计""" + conn = get_db() + cursor = conn.cursor() + + # 比赛统计 + cursor.execute('SELECT COUNT(*) as total FROM matches') + total_matches = cursor.fetchone()['total'] + + cursor.execute('SELECT COUNT(*) as total FROM matches WHERE status = "finished"') + finished_matches = cursor.fetchone()['total'] + + # 算法统计 + cursor.execute('SELECT COUNT(*) as total FROM algorithms WHERE is_active = 1') + active_algorithms = cursor.fetchone()['total'] + + # 预测统计 + cursor.execute('SELECT COUNT(*) as total FROM predictions') + total_predictions = cursor.fetchone()['total'] + + cursor.execute('SELECT COUNT(*) as total FROM predictions WHERE is_correct = 1') + correct_predictions = cursor.fetchone()['total'] + + overall_accuracy = (correct_predictions / total_predictions * 100) if total_predictions > 0 else 0 + + conn.close() + + return jsonify({ + 'success': True, + 'data': { + 'total_matches': total_matches, + 'finished_matches': finished_matches, + 'upcoming_matches': total_matches - finished_matches, + 'active_algorithms': active_algorithms, + 'total_predictions': total_predictions, + 'correct_predictions': correct_predictions, + 'overall_accuracy': round(overall_accuracy, 2) + } + }) + + +if __name__ == '__main__': + init_db() + app.run(host='0.0.0.0', port=16015, debug=False) \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..1a72c35 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,2 @@ +flask>=2.0.0 +flask-cors>=3.0.0 \ No newline at end of file diff --git a/backend/worldcup.db b/backend/worldcup.db new file mode 100644 index 0000000..52f2845 Binary files /dev/null and b/backend/worldcup.db differ diff --git a/frontend/admin.html b/frontend/admin.html new file mode 100644 index 0000000..43ccd13 --- /dev/null +++ b/frontend/admin.html @@ -0,0 +1,264 @@ + + + + + + 管理后台 - 世界杯预测系统 + + + +
+
+

⚙️ 管理后台

+ +
+ + +
+

📊 系统配置

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+ + + +
+ + +
+
+

🤖 预测算法管理

+ +
+ + + + + + + + + + + + + + + + +
ID名称描述准确率预测数正确数状态操作
+
+ + +
+
+

⚽ 比赛管理

+ +
+ + + + + + + + + + + + + + + + +
代码主队客队时间比分轮次状态操作
+
+ + +
+
+

🎯 预测管理

+
+ +
+
+
+ + +
+ + + + + + + + + + + + + + + + +
比赛算法预测比分预测胜负置信度实际比分结果操作
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/css/style.css b/frontend/css/style.css new file mode 100644 index 0000000..8f8e14e --- /dev/null +++ b/frontend/css/style.css @@ -0,0 +1,572 @@ +/* 全局样式 */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + color: #e0e0e0; + min-height: 100vh; + line-height: 1.6; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 20px; +} + +/* 头部导航 */ +header { + background: rgba(255, 255, 255, 0.05); + backdrop-filter: blur(10px); + border-radius: 15px; + padding: 20px 30px; + margin-bottom: 30px; + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); +} + +header h1 { + font-size: 28px; + background: linear-gradient(135deg, #00b894, #00cec9); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +nav a { + color: #a0a0a0; + text-decoration: none; + margin-left: 20px; + padding: 8px 16px; + border-radius: 8px; + transition: all 0.3s; +} + +nav a:hover, nav a.active { + background: rgba(0, 206, 201, 0.2); + color: #00cec9; +} + +/* 统计卡片 */ +.stats-overview { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 20px; + margin-bottom: 30px; +} + +.stat-card { + background: rgba(255, 255, 255, 0.05); + backdrop-filter: blur(10px); + border-radius: 15px; + padding: 25px; + text-align: center; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + transition: transform 0.3s; +} + +.stat-card:hover { + transform: translateY(-5px); +} + +.stat-card.highlight { + background: linear-gradient(135deg, rgba(0, 184, 148, 0.2), rgba(0, 206, 201, 0.2)); + border: 1px solid rgba(0, 206, 201, 0.3); +} + +.stat-value { + font-size: 36px; + font-weight: bold; + background: linear-gradient(135deg, #00b894, #00cec9); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.stat-label { + color: #a0a0a0; + margin-top: 5px; + font-size: 14px; +} + +/* 标签切换 */ +.tabs { + display: flex; + gap: 10px; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.tab-btn { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + color: #a0a0a0; + padding: 12px 24px; + border-radius: 10px; + cursor: pointer; + transition: all 0.3s; + font-size: 14px; +} + +.tab-btn:hover { + background: rgba(0, 206, 201, 0.1); + border-color: rgba(0, 206, 201, 0.3); +} + +.tab-btn.active { + background: linear-gradient(135deg, #00b894, #00cec9); + color: #fff; + border-color: transparent; +} + +.tab-content { + display: none; +} + +.tab-content.active { + display: block; +} + +/* 比赛卡片网格 */ +.matches-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 20px; +} + +.match-card { + background: rgba(255, 255, 255, 0.05); + backdrop-filter: blur(10px); + border-radius: 15px; + padding: 20px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + transition: all 0.3s; + cursor: pointer; +} + +.match-card:hover { + transform: translateY(-3px); + box-shadow: 0 12px 40px rgba(0, 206, 201, 0.2); +} + +.match-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; +} + +.match-round { + background: rgba(0, 206, 201, 0.2); + color: #00cec9; + padding: 4px 12px; + border-radius: 20px; + font-size: 12px; +} + +.match-time { + color: #a0a0a0; + font-size: 12px; +} + +.match-teams { + display: flex; + justify-content: space-between; + align-items: center; + margin: 15px 0; +} + +.team { + text-align: center; + flex: 1; +} + +.team-name { + font-size: 16px; + font-weight: bold; + margin-bottom: 5px; +} + +.team-flag { + font-size: 32px; +} + +.vs { + font-size: 14px; + color: #a0a0a0; + margin: 0 10px; +} + +.match-score { + display: flex; + justify-content: center; + align-items: center; + gap: 20px; + margin: 15px 0; + font-size: 28px; + font-weight: bold; +} + +.score-box { + background: rgba(0, 206, 201, 0.1); + padding: 10px 20px; + border-radius: 10px; +} + +.match-predictions { + margin-top: 15px; + padding-top: 15px; + border-top: 1px solid rgba(255, 255, 255, 0.1); +} + +.prediction-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 0; + font-size: 13px; +} + +.prediction-algorithm { + color: #00cec9; +} + +.prediction-result { + background: rgba(0, 206, 201, 0.1); + padding: 2px 8px; + border-radius: 5px; +} + +.prediction-result.correct { + background: rgba(46, 213, 115, 0.2); + color: #2ed573; +} + +.prediction-result.wrong { + background: rgba(255, 71, 87, 0.2); + color: #ff4757; +} + +/* 算法卡片 */ +.algorithms-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 20px; +} + +.algorithm-card { + background: rgba(255, 255, 255, 0.05); + backdrop-filter: blur(10px); + border-radius: 15px; + padding: 25px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + transition: all 0.3s; + cursor: pointer; +} + +.algorithm-card:hover { + transform: translateY(-3px); + box-shadow: 0 12px 40px rgba(0, 206, 201, 0.2); +} + +.algorithm-name { + font-size: 20px; + font-weight: bold; + margin-bottom: 10px; + color: #00cec9; +} + +.algorithm-desc { + color: #a0a0a0; + font-size: 13px; + margin-bottom: 15px; +} + +.algorithm-stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px; + text-align: center; +} + +.algo-stat { + padding: 10px; + background: rgba(0, 0, 0, 0.2); + border-radius: 8px; +} + +.algo-stat-value { + font-size: 18px; + font-weight: bold; + color: #00cec9; +} + +.algo-stat-label { + font-size: 11px; + color: #a0a0a0; +} + +/* 弹窗 */ +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.8); + overflow: auto; +} + +.modal-content { + background: linear-gradient(135deg, #1a1a2e, #16213e); + margin: 5% auto; + padding: 30px; + border-radius: 15px; + width: 90%; + max-width: 600px; + position: relative; + box-shadow: 0 10px 40px rgba(0, 206, 201, 0.2); + max-height: 80vh; + overflow-y: auto; +} + +.close { + position: absolute; + right: 20px; + top: 15px; + color: #a0a0a0; + font-size: 28px; + font-weight: bold; + cursor: pointer; + transition: color 0.3s; +} + +.close:hover { + color: #ff4757; +} + +/* 表单样式 */ +.form-group { + margin-bottom: 20px; +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 15px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + color: #a0a0a0; + font-size: 14px; +} + +.form-group input, +.form-group select, +.form-group textarea { + width: 100%; + padding: 12px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + color: #e0e0e0; + font-size: 14px; + transition: all 0.3s; +} + +.form-group input:focus, +.form-group select:focus, +.form-group textarea:focus { + outline: none; + border-color: #00cec9; + box-shadow: 0 0 0 3px rgba(0, 206, 201, 0.1); +} + +.form-group input[type="range"] { + padding: 0; + height: 8px; +} + +/* 按钮 */ +.btn-primary { + background: linear-gradient(135deg, #00b894, #00cec9); + color: #fff; + border: none; + padding: 12px 24px; + border-radius: 8px; + cursor: pointer; + font-size: 14px; + font-weight: bold; + transition: all 0.3s; +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(0, 206, 201, 0.4); +} + +.btn-secondary { + background: rgba(255, 255, 255, 0.1); + color: #e0e0e0; + border: 1px solid rgba(255, 255, 255, 0.2); + padding: 10px 20px; + border-radius: 8px; + cursor: pointer; + font-size: 14px; + transition: all 0.3s; +} + +.btn-secondary:hover { + background: rgba(255, 255, 255, 0.2); +} + +.btn-danger { + background: rgba(255, 71, 87, 0.2); + color: #ff4757; + border: 1px solid rgba(255, 71, 87, 0.3); + padding: 6px 12px; + border-radius: 5px; + cursor: pointer; + font-size: 12px; + transition: all 0.3s; +} + +.btn-danger:hover { + background: rgba(255, 71, 87, 0.4); +} + +.btn-small { + padding: 6px 12px; + font-size: 12px; +} + +/* 表格 */ +.data-table { + width: 100%; + border-collapse: collapse; + background: rgba(255, 255, 255, 0.05); + border-radius: 10px; + overflow: hidden; +} + +.data-table th, +.data-table td { + padding: 12px 15px; + text-align: left; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.data-table th { + background: rgba(0, 206, 201, 0.1); + color: #00cec9; + font-weight: bold; + font-size: 13px; +} + +.data-table td { + font-size: 13px; +} + +.data-table tr:hover { + background: rgba(0, 206, 201, 0.05); +} + +/* 管理后台专用样式 */ +.admin-section { + background: rgba(255, 255, 255, 0.05); + border-radius: 15px; + padding: 25px; + margin-bottom: 20px; +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +.config-form { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 15px; + align-items: end; +} + +/* 状态标签 */ +.status-badge { + display: inline-block; + padding: 4px 12px; + border-radius: 20px; + font-size: 12px; +} + +.status-upcoming { + background: rgba(255, 193, 7, 0.2); + color: #ffc107; +} + +.status-finished { + background: rgba(46, 213, 115, 0.2); + color: #2ed573; +} + +.status-active { + background: rgba(0, 206, 201, 0.2); + color: #00cec9; +} + +.status-inactive { + background: rgba(255, 71, 87, 0.2); + color: #ff4757; +} + +/* 响应式 */ +@media (max-width: 768px) { + header { + flex-direction: column; + text-align: center; + } + + nav { + margin-top: 15px; + } + + nav a { + margin: 5px; + } + + .form-row { + grid-template-columns: 1fr; + } + + .match-score { + font-size: 20px; + } + + .stats-overview { + grid-template-columns: 1fr 1fr; + } +} + +@media (max-width: 480px) { + .stats-overview { + grid-template-columns: 1fr; + } + + .matches-grid, + .algorithms-grid { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..1e61d37 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,82 @@ + + + + + + 世界杯预测系统 + + + +
+
+

🏆 世界杯预测系统

+ +
+ + +
+
+
0
+
总比赛数
+
+
+
0
+
即将进行
+
+
+
0
+
预测算法
+
+
+
0%
+
总体准确率
+
+
+ + +
+ + + +
+ + +
+

📅 即将进行的比赛

+
+ +
+
+ + +
+

✅ 已完成的比赛

+
+ +
+
+ + +
+

🤖 预测算法排行榜

+
+ +
+
+
+ + + + + + + \ No newline at end of file diff --git a/frontend/js/admin.js b/frontend/js/admin.js new file mode 100644 index 0000000..9898801 --- /dev/null +++ b/frontend/js/admin.js @@ -0,0 +1,513 @@ +// API基础路径 +const API_BASE = ''; + +// 页面加载完成后执行 +document.addEventListener('DOMContentLoaded', () => { + loadConfig(); + loadAlgorithms(); + loadMatches(); + loadPredictions(); + initTabs(); + initForms(); +}); + +// 初始化标签切换 +function initTabs() { + document.querySelectorAll('.tab-btn').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); + document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); + + btn.classList.add('active'); + const tabId = btn.dataset.tab + '-tab'; + document.getElementById(tabId).classList.add('active'); + }); + }); +} + +// 初始化表单 +function initForms() { + // 算法表单 + document.getElementById('algorithm-form').addEventListener('submit', async (e) => { + e.preventDefault(); + + const id = document.getElementById('algorithm-id').value; + const data = { + name: document.getElementById('algorithm-name').value, + description: document.getElementById('algorithm-desc').value, + is_active: parseInt(document.getElementById('algorithm-active').value) + }; + + try { + const url = id ? `${API_BASE}/api/algorithms/${id}` : `${API_BASE}/api/algorithms`; + const method = id ? 'PUT' : 'POST'; + + const response = await fetch(url, { + method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + + const result = await response.json(); + + if (result.success) { + closeModal('algorithm-modal'); + loadAlgorithms(); + alert(id ? '算法更新成功' : '算法添加成功'); + } else { + alert(result.message || '操作失败'); + } + } catch (error) { + console.error('保存算法失败:', error); + alert('操作失败,请重试'); + } + }); + + // 比赛表单 + document.getElementById('match-form').addEventListener('submit', async (e) => { + e.preventDefault(); + + const id = document.getElementById('match-id').value; + const data = { + match_code: document.getElementById('match-code').value, + home_team: document.getElementById('home-team').value, + away_team: document.getElementById('away-team').value, + match_time: document.getElementById('match-time').value || null, + round_name: document.getElementById('match-round').value, + status: document.getElementById('match-status').value, + home_score: document.getElementById('home-score').value || null, + away_score: document.getElementById('away-score').value || null + }; + + try { + const url = id ? `${API_BASE}/api/matches/${id}` : `${API_BASE}/api/matches`; + const method = id ? 'PUT' : 'POST'; + + const response = await fetch(url, { + method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + + const result = await response.json(); + + if (result.success) { + closeModal('match-modal'); + loadMatches(); + loadPredictions(); + alert(id ? '比赛更新成功' : '比赛添加成功'); + } else { + alert(result.message || '操作失败'); + } + } catch (error) { + console.error('保存比赛失败:', error); + alert('操作失败,请重试'); + } + }); + + // 预测表单 + document.getElementById('prediction-form').addEventListener('submit', async (e) => { + e.preventDefault(); + + const data = { + match_id: document.getElementById('prediction-match').value, + algorithm_id: document.getElementById('prediction-algorithm').value, + predicted_home_score: document.getElementById('pred-home-score').value || null, + predicted_away_score: document.getElementById('pred-away-score').value || null, + predicted_winner: document.getElementById('pred-winner').value || null, + confidence: document.getElementById('pred-confidence').value / 100 + }; + + try { + const response = await fetch(`${API_BASE}/api/predictions`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + + const result = await response.json(); + + if (result.success) { + closeModal('prediction-modal'); + loadPredictions(); + alert('预测添加成功'); + } else { + alert(result.message || '操作失败'); + } + } catch (error) { + console.error('保存预测失败:', error); + alert('操作失败,请重试'); + } + }); + + // 置信度滑块 + document.getElementById('pred-confidence').addEventListener('input', (e) => { + document.getElementById('confidence-value').textContent = e.target.value + '%'; + }); +} + +// 加载配置 +async function loadConfig() { + try { + const response = await fetch(`${API_BASE}/api/config`); + const result = await response.json(); + + if (result.success) { + document.getElementById('prediction-cycle').value = result.data.prediction_cycle_hours || 24; + document.getElementById('last-prediction-time').value = result.data.last_prediction_time || ''; + document.getElementById('world-cup-year').value = result.data.world_cup_year || '2026'; + } + } catch (error) { + console.error('加载配置失败:', error); + } +} + +// 保存配置 +async function saveConfig() { + const data = { + prediction_cycle_hours: document.getElementById('prediction-cycle').value, + last_prediction_time: document.getElementById('last-prediction-time').value, + world_cup_year: document.getElementById('world-cup-year').value + }; + + try { + const response = await fetch(`${API_BASE}/api/config`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + + const result = await response.json(); + + if (result.success) { + alert('配置保存成功'); + } else { + alert('保存失败'); + } + } catch (error) { + console.error('保存配置失败:', error); + alert('保存失败,请重试'); + } +} + +// 加载算法列表 +async function loadAlgorithms() { + try { + const response = await fetch(`${API_BASE}/api/algorithms`); + const result = await response.json(); + + if (result.success) { + const tbody = document.getElementById('algorithms-table'); + + if (result.data.length === 0) { + tbody.innerHTML = '暂无算法数据'; + return; + } + + tbody.innerHTML = result.data.map(algo => ` + + ${algo.id} + ${algo.name} + ${algo.description || '-'} + ${(algo.accuracy || 0).toFixed(1)}% + ${algo.total_predictions || 0} + ${algo.correct_predictions || 0} + + + ${algo.is_active ? '启用' : '禁用'} + + + + + + + + `).join(''); + } + } catch (error) { + console.error('加载算法失败:', error); + } +} + +// 加载比赛列表 +async function loadMatches() { + try { + const response = await fetch(`${API_BASE}/api/matches`); + const result = await response.json(); + + if (result.success) { + const tbody = document.getElementById('matches-table'); + + if (result.data.length === 0) { + tbody.innerHTML = '暂无比赛数据'; + return; + } + + tbody.innerHTML = result.data.map(match => ` + + ${match.match_code} + ${match.home_team} + ${match.away_team} + ${match.match_time ? new Date(match.match_time).toLocaleString('zh-CN') : '待定'} + ${match.home_score !== null ? `${match.home_score} : ${match.away_score}` : '-'} + ${match.round_name || '小组赛'} + + + ${match.status === 'finished' ? '已完成' : '即将进行'} + + + + + + + + `).join(''); + + // 更新预测管理的比赛筛选下拉框 + const filterSelect = document.getElementById('prediction-match-filter'); + const currentValue = filterSelect.value; + filterSelect.innerHTML = '' + + result.data.map(m => ``).join(''); + filterSelect.value = currentValue; + + // 更新预测表单的比赛下拉框 + const predMatchSelect = document.getElementById('prediction-match'); + predMatchSelect.innerHTML = '' + + result.data.map(m => ``).join(''); + } + } catch (error) { + console.error('加载比赛失败:', error); + } +} + +// 加载预测列表 +async function loadPredictions() { + try { + const matchFilter = document.getElementById('prediction-match-filter').value; + + // 先获取所有比赛 + const matchesResponse = await fetch(`${API_BASE}/api/matches`); + const matchesResult = await matchesResponse.json(); + + if (!matchesResult.success) return; + + // 获取所有算法 + const algorithmsResponse = await fetch(`${API_BASE}/api/algorithms`); + const algorithmsResult = await algorithmsResponse.json(); + + if (!algorithmsResult.success) return; + + const algorithms = algorithmsResult.data; + + // 获取每场比赛的预测 + let allPredictions = []; + for (const match of matchesResult.data) { + if (matchFilter && match.id != matchFilter) continue; + + const predResponse = await fetch(`${API_BASE}/api/matches/${match.id}`); + const predResult = await predResponse.json(); + + if (predResult.success && predResult.data.predictions.length > 0) { + predResult.data.predictions.forEach(pred => { + allPredictions.push({ + ...pred, + match: match + }); + }); + } + } + + const tbody = document.getElementById('predictions-table'); + + if (allPredictions.length === 0) { + tbody.innerHTML = '暂无预测数据'; + return; + } + + tbody.innerHTML = allPredictions.map(pred => ` + + ${pred.match.home_team} vs ${pred.match.away_team} + ${pred.algorithm_name} + ${pred.predicted_home_score !== null ? `${pred.predicted_home_score} : ${pred.predicted_away_score}` : '-'} + ${pred.predicted_winner || '-'} + ${pred.confidence ? (pred.confidence * 100).toFixed(0) + '%' : '-'} + ${pred.match.home_score !== null ? `${pred.match.home_score} : ${pred.match.away_score}` : '-'} + + ${pred.is_correct !== null ? + ` + ${pred.is_correct ? '正确' : '错误'} + ` : + '待验证' + } + + + + + + `).join(''); + + // 更新预测表单的算法下拉框 + const predAlgoSelect = document.getElementById('prediction-algorithm'); + predAlgoSelect.innerHTML = '' + + algorithms.map(a => ``).join(''); + } catch (error) { + console.error('加载预测失败:', error); + } +} + +// 显示添加算法弹窗 +function showAddAlgorithmModal() { + document.getElementById('algorithm-modal-title').textContent = '添加算法'; + document.getElementById('algorithm-form').reset(); + document.getElementById('algorithm-id').value = ''; + document.getElementById('algorithm-modal').style.display = 'block'; +} + +// 编辑算法 +async function editAlgorithm(id) { + try { + const response = await fetch(`${API_BASE}/api/algorithms/${id}`); + const result = await response.json(); + + if (result.success) { + const algo = result.data.algorithm; + document.getElementById('algorithm-modal-title').textContent = '编辑算法'; + document.getElementById('algorithm-id').value = algo.id; + document.getElementById('algorithm-name').value = algo.name; + document.getElementById('algorithm-desc').value = algo.description || ''; + document.getElementById('algorithm-active').value = algo.is_active ? '1' : '0'; + document.getElementById('algorithm-modal').style.display = 'block'; + } + } catch (error) { + console.error('加载算法失败:', error); + } +} + +// 删除算法 +async function deleteAlgorithm(id) { + if (!confirm('确定要删除这个算法吗?相关的预测数据也会被删除。')) { + return; + } + + try { + const response = await fetch(`${API_BASE}/api/algorithms/${id}`, { + method: 'DELETE' + }); + + const result = await response.json(); + + if (result.success) { + loadAlgorithms(); + loadPredictions(); + alert('删除成功'); + } else { + alert('删除失败'); + } + } catch (error) { + console.error('删除算法失败:', error); + alert('删除失败,请重试'); + } +} + +// 显示添加比赛弹窗 +function showAddMatchModal() { + document.getElementById('match-modal-title').textContent = '添加比赛'; + document.getElementById('match-form').reset(); + document.getElementById('match-id').value = ''; + document.getElementById('match-modal').style.display = 'block'; +} + +// 编辑比赛 +async function editMatch(id) { + try { + const response = await fetch(`${API_BASE}/api/matches/${id}`); + const result = await response.json(); + + if (result.success) { + const match = result.data.match; + document.getElementById('match-modal-title').textContent = '编辑比赛'; + document.getElementById('match-id').value = match.id; + document.getElementById('match-code').value = match.match_code; + document.getElementById('home-team').value = match.home_team; + document.getElementById('away-team').value = match.away_team; + document.getElementById('match-time').value = match.match_time || ''; + document.getElementById('match-round').value = match.round_name || '小组赛'; + document.getElementById('match-status').value = match.status || 'upcoming'; + document.getElementById('home-score').value = match.home_score !== null ? match.home_score : ''; + document.getElementById('away-score').value = match.away_score !== null ? match.away_score : ''; + document.getElementById('match-modal').style.display = 'block'; + } + } catch (error) { + console.error('加载比赛失败:', error); + } +} + +// 删除比赛 +async function deleteMatch(id) { + if (!confirm('确定要删除这场比赛吗?相关的预测数据也会被删除。')) { + return; + } + + try { + const response = await fetch(`${API_BASE}/api/matches/${id}`, { + method: 'DELETE' + }); + + const result = await response.json(); + + if (result.success) { + loadMatches(); + loadPredictions(); + alert('删除成功'); + } else { + alert('删除失败'); + } + } catch (error) { + console.error('删除比赛失败:', error); + alert('删除失败,请重试'); + } +} + +// 为比赛添加预测 +function addPredictionForMatch(matchId) { + document.getElementById('prediction-match').value = matchId; + document.getElementById('prediction-modal').style.display = 'block'; +} + +// 评估预测结果 +async function evaluatePredictions() { + if (!confirm('确定要评估所有已完成比赛的预测结果吗?')) { + return; + } + + try { + const response = await fetch(`${API_BASE}/api/predictions/evaluate`, { + method: 'POST' + }); + + const result = await response.json(); + + if (result.success) { + alert(`评估完成,共评估了 ${result.evaluated} 条预测`); + loadPredictions(); + loadAlgorithms(); + } else { + alert('评估失败'); + } + } catch (error) { + console.error('评估预测失败:', error); + alert('评估失败,请重试'); + } +} + +// 关闭弹窗 +function closeModal(modalId) { + document.getElementById(modalId).style.display = 'none'; +} + +// 点击弹窗外部关闭 +window.addEventListener('click', (e) => { + if (e.target.classList.contains('modal')) { + e.target.style.display = 'none'; + } +}); \ No newline at end of file diff --git a/frontend/js/app.js b/frontend/js/app.js new file mode 100644 index 0000000..0df4c33 --- /dev/null +++ b/frontend/js/app.js @@ -0,0 +1,374 @@ +// API基础路径 +const API_BASE = ''; + +// 页面加载完成后执行 +document.addEventListener('DOMContentLoaded', () => { + loadOverviewStats(); + loadUpcomingMatches(); + loadFinishedMatches(); + loadAlgorithms(); + initTabs(); +}); + +// 初始化标签切换 +function initTabs() { + document.querySelectorAll('.tab-btn').forEach(btn => { + btn.addEventListener('click', () => { + // 移除所有活动状态 + document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); + document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); + + // 添加当前活动状态 + btn.classList.add('active'); + const tabId = btn.dataset.tab + '-tab'; + document.getElementById(tabId).classList.add('active'); + }); + }); +} + +// 加载总体统计 +async function loadOverviewStats() { + try { + const response = await fetch(`${API_BASE}/api/stats/overview`); + const result = await response.json(); + + if (result.success) { + document.getElementById('total-matches').textContent = result.data.total_matches; + document.getElementById('upcoming-matches').textContent = result.data.upcoming_matches; + document.getElementById('active-algorithms').textContent = result.data.active_algorithms; + document.getElementById('overall-accuracy').textContent = result.data.overall_accuracy.toFixed(1) + '%'; + } + } catch (error) { + console.error('加载统计数据失败:', error); + } +} + +// 加载即将进行的比赛 +async function loadUpcomingMatches() { + try { + const response = await fetch(`${API_BASE}/api/matches?status=upcoming`); + const result = await response.json(); + + if (result.success) { + const container = document.getElementById('upcoming-matches-list'); + + if (result.data.length === 0) { + container.innerHTML = '

暂无即将进行的比赛

'; + return; + } + + // 获取每场比赛的预测 + for (const match of result.data) { + const predictions = await getMatchPredictions(match.id); + match.predictions = predictions; + } + + container.innerHTML = result.data.map(match => renderMatchCard(match, false)).join(''); + } + } catch (error) { + console.error('加载比赛失败:', error); + } +} + +// 加载已完成的比赛 +async function loadFinishedMatches() { + try { + const response = await fetch(`${API_BASE}/api/matches?status=finished`); + const result = await response.json(); + + if (result.success) { + const container = document.getElementById('finished-matches-list'); + + if (result.data.length === 0) { + container.innerHTML = '

暂无已完成的比赛

'; + return; + } + + // 获取每场比赛的预测 + for (const match of result.data) { + const predictions = await getMatchPredictions(match.id); + match.predictions = predictions; + } + + container.innerHTML = result.data.map(match => renderMatchCard(match, true)).join(''); + } + } catch (error) { + console.error('加载比赛失败:', error); + } +} + +// 获取比赛预测 +async function getMatchPredictions(matchId) { + try { + const response = await fetch(`${API_BASE}/api/matches/${matchId}`); + const result = await response.json(); + return result.success ? result.data.predictions : []; + } catch (error) { + console.error('获取预测失败:', error); + return []; + } +} + +// 渲染比赛卡片 +function renderMatchCard(match, isFinished) { + const matchTime = match.match_time ? new Date(match.match_time).toLocaleString('zh-CN') : '待定'; + + let predictionsHtml = ''; + if (match.predictions && match.predictions.length > 0) { + predictionsHtml = ` +
+

预测结果

+ ${match.predictions.map(pred => { + let resultText = ''; + let resultClass = ''; + if (isFinished && pred.is_correct !== null) { + resultText = pred.is_correct ? '✓ 正确' : '✗ 错误'; + resultClass = pred.is_correct ? 'correct' : 'wrong'; + } + + return ` +
+ ${pred.algorithm_name} + + ${pred.predicted_home_score !== null ? `${pred.predicted_home_score} : ${pred.predicted_away_score}` : ''} + ${pred.predicted_winner ? `(${getWinnerText(pred.predicted_winner, match.home_team, match.away_team)})` : ''} + + ${resultText ? `${resultText}` : ''} +
+ `; + }).join('')} +
+ `; + } + + return ` +
+
+ ${match.round_name || '小组赛'} + ${matchTime} +
+
+
+
${match.home_team}
+
+
VS
+
+
${match.away_team}
+
+
+ ${isFinished && match.home_score !== null ? ` +
+ ${match.home_score} + : + ${match.away_score} +
+ ` : ''} + ${predictionsHtml} +
+ `; +} + +// 获取胜负文本 +function getWinnerText(winner, homeTeam, awayTeam) { + if (winner === 'home') return homeTeam + '胜'; + if (winner === 'away') return awayTeam + '胜'; + if (winner === 'draw') return '平局'; + return winner; +} + +// 加载算法排行 +async function loadAlgorithms() { + try { + const response = await fetch(`${API_BASE}/api/algorithms`); + const result = await response.json(); + + if (result.success) { + const container = document.getElementById('algorithms-list'); + + if (result.data.length === 0) { + container.innerHTML = '

暂无算法数据

'; + return; + } + + container.innerHTML = result.data.map(algo => ` +
+
${algo.name}
+
${algo.description || '暂无描述'}
+
+
+
${(algo.accuracy || 0).toFixed(1)}%
+
准确率
+
+
+
${algo.total_predictions || 0}
+
预测数
+
+
+
${algo.correct_predictions || 0}
+
正确数
+
+
+
+ `).join(''); + } + } catch (error) { + console.error('加载算法失败:', error); + } +} + +// 显示比赛详情 +async function showMatchDetail(matchId) { + try { + const response = await fetch(`${API_BASE}/api/matches/${matchId}`); + const result = await response.json(); + + if (result.success) { + const match = result.data.match; + const predictions = result.data.predictions; + + const matchTime = match.match_time ? new Date(match.match_time).toLocaleString('zh-CN') : '待定'; + + let predictionsHtml = ''; + if (predictions.length > 0) { + predictionsHtml = ` +

预测详情

+
+ ${predictions.map(pred => ` +
+
+ ${pred.algorithm_name} + 准确率: ${(pred.algorithm_accuracy || 0).toFixed(1)}% +
+
+ ${pred.predicted_home_score !== null ? + `预测比分: ${pred.predicted_home_score} : ${pred.predicted_away_score}` : + ''} + ${pred.predicted_winner ? + `
预测胜负: ${getWinnerText(pred.predicted_winner, match.home_team, match.away_team)}` : + ''} + ${pred.confidence ? + `
置信度: ${(pred.confidence * 100).toFixed(0)}%` : + ''} +
+ ${match.status === 'finished' && pred.is_correct !== null ? ` +
+ ${pred.is_correct ? '✓ 预测正确' : '✗ 预测错误'} +
+ ` : ''} +
+ `).join('')} +
+ `; + } + + document.getElementById('modal-title').textContent = + `${match.home_team} vs ${match.away_team}`; + + document.getElementById('modal-body').innerHTML = ` +
+ + ${match.status === 'finished' ? '已完成' : '即将进行'} + +
+
+ ${match.round_name || '小组赛'} | ${matchTime} +
+ ${match.status === 'finished' && match.home_score !== null ? ` +
+ ${match.home_score} + : + ${match.away_score} +
+ ` : ''} + ${predictionsHtml} + `; + + document.getElementById('match-modal').style.display = 'block'; + } + } catch (error) { + console.error('加载比赛详情失败:', error); + } +} + +// 显示算法详情 +async function showAlgorithmDetail(algoId) { + try { + const response = await fetch(`${API_BASE}/api/algorithms/${algoId}`); + const result = await response.json(); + + if (result.success) { + const algo = result.data.algorithm; + const history = result.data.history; + + let historyHtml = ''; + if (history.length > 0) { + historyHtml = ` +

预测历史

+
+ ${history.map(h => ` +
+
+ ${h.home_team} vs ${h.away_team} + + ${h.match_time ? new Date(h.match_time).toLocaleDateString('zh-CN') : '待定'} + +
+
+ 预测: ${h.predicted_home_score} : ${h.predicted_away_score} + ${h.home_score !== null ? ` | 实际: ${h.home_score} : ${h.away_score}` : ''} +
+ ${h.is_correct !== null ? ` + + ${h.is_correct ? '正确' : '错误'} + + ` : ''} +
+ `).join('')} +
+ `; + } + + document.getElementById('modal-title').textContent = algo.name; + document.getElementById('modal-body').innerHTML = ` +

${algo.description || '暂无描述'}

+
+
+
${(algo.accuracy || 0).toFixed(1)}%
+
准确率
+
+
+
${algo.total_predictions || 0}
+
总预测数
+
+
+
${algo.correct_predictions || 0}
+
正确预测
+
+
+ ${historyHtml} + `; + + document.getElementById('match-modal').style.display = 'block'; + } + } catch (error) { + console.error('加载算法详情失败:', error); + } +} + +// 关闭弹窗 +document.querySelectorAll('.close').forEach(btn => { + btn.addEventListener('click', () => { + btn.parentElement.parentElement.style.display = 'none'; + }); +}); + +// 点击弹窗外部关闭 +window.addEventListener('click', (e) => { + if (e.target.classList.contains('modal')) { + e.target.style.display = 'none'; + } +}); \ No newline at end of file diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..50f6a5d --- /dev/null +++ b/start.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# 世界杯预测系统启动脚本 + +cd "$(dirname "$0")" + +# 安装依赖 +pip3 install -r backend/requirements.txt -q + +# 启动服务 +echo "Starting World Cup Predictor on port 16015..." +python3 backend/app.py \ No newline at end of file diff --git a/worldcup.log b/worldcup.log new file mode 100644 index 0000000..aea534c --- /dev/null +++ b/worldcup.log @@ -0,0 +1,4 @@ +Traceback (most recent call last): + File "/home/openclaw/.openclaw/workspace-hz4th_coder/works/worldcup-predictor/backend/app.py", line 8, in + from flask import Flask, jsonify, request, send_from_directory +ModuleNotFoundError: No module named 'flask'