589 lines
22 KiB
JavaScript
589 lines
22 KiB
JavaScript
// 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 = '<tr><td colspan="8" style="text-align: center; color: #a0a0a0;">暂无算法数据</td></tr>';
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = result.data.map(algo => `
|
||
<tr>
|
||
<td>${algo.id}</td>
|
||
<td><strong>${algo.name}</strong></td>
|
||
<td>${algo.description || '-'}</td>
|
||
<td><span style="color: #00cec9;">${(algo.accuracy || 0).toFixed(1)}%</span></td>
|
||
<td>${algo.total_predictions || 0}</td>
|
||
<td>${algo.correct_predictions || 0}</td>
|
||
<td>
|
||
<span class="status-badge ${algo.is_active ? 'status-active' : 'status-inactive'}">
|
||
${algo.is_active ? '启用' : '禁用'}
|
||
</span>
|
||
</td>
|
||
<td>
|
||
<button onclick="editAlgorithm(${algo.id})" class="btn-secondary btn-small">编辑</button>
|
||
<button onclick="deleteAlgorithm(${algo.id})" class="btn-danger btn-small" style="margin-left: 5px;">删除</button>
|
||
</td>
|
||
</tr>
|
||
`).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 = '<tr><td colspan="8" style="text-align: center; color: #a0a0a0;">暂无比赛数据</td></tr>';
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = result.data.map(match => `
|
||
<tr>
|
||
<td>${match.match_code}</td>
|
||
<td>${match.home_team}</td>
|
||
<td>${match.away_team}</td>
|
||
<td>${match.match_time ? new Date(match.match_time).toLocaleString('zh-CN') : '待定'}</td>
|
||
<td>${match.home_score !== null ? `${match.home_score} : ${match.away_score}` : '-'}</td>
|
||
<td>${match.round_name || '小组赛'}</td>
|
||
<td>
|
||
<span class="status-badge ${match.status === 'finished' ? 'status-finished' : 'status-upcoming'}">
|
||
${match.status === 'finished' ? '已完成' : '即将进行'}
|
||
</span>
|
||
</td>
|
||
<td>
|
||
<button onclick="editMatch(${match.id})" class="btn-secondary btn-small">编辑</button>
|
||
<button onclick="deleteMatch(${match.id})" class="btn-danger btn-small" style="margin-left: 5px;">删除</button>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
|
||
// 更新预测管理的比赛筛选下拉框
|
||
const filterSelect = document.getElementById('prediction-match-filter');
|
||
const currentValue = filterSelect.value;
|
||
filterSelect.innerHTML = '<option value="">全部比赛</option>' +
|
||
result.data.map(m => `<option value="${m.id}">${m.home_team} vs ${m.away_team}</option>`).join('');
|
||
filterSelect.value = currentValue;
|
||
|
||
// 更新预测表单的比赛下拉框
|
||
const predMatchSelect = document.getElementById('prediction-match');
|
||
predMatchSelect.innerHTML = '<option value="">-- 选择比赛 --</option>' +
|
||
result.data.map(m => `<option value="${m.id}">${m.home_team} vs ${m.away_team}</option>`).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 = '<tr><td colspan="8" style="text-align: center; color: #a0a0a0;">暂无预测数据</td></tr>';
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = allPredictions.map(pred => `
|
||
<tr>
|
||
<td>${pred.match.home_team} vs ${pred.match.away_team}</td>
|
||
<td>${pred.algorithm_name}</td>
|
||
<td>${pred.predicted_home_score !== null ? `${pred.predicted_home_score} : ${pred.predicted_away_score}` : '-'}</td>
|
||
<td>${pred.predicted_winner || '-'}</td>
|
||
<td>${pred.confidence ? (pred.confidence * 100).toFixed(0) + '%' : '-'}</td>
|
||
<td>${pred.match.home_score !== null ? `${pred.match.home_score} : ${pred.match.away_score}` : '-'}</td>
|
||
<td>
|
||
${pred.is_correct !== null ?
|
||
`<span class="status-badge ${pred.is_correct ? 'status-active' : 'status-inactive'}">
|
||
${pred.is_correct ? '正确' : '错误'}
|
||
</span>` :
|
||
'<span style="color: #a0a0a0;">待验证</span>'
|
||
}
|
||
</td>
|
||
<td>
|
||
<button onclick="addPredictionForMatch(${pred.match.id})" class="btn-secondary btn-small">修改</button>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
|
||
// 更新预测表单的算法下拉框
|
||
const predAlgoSelect = document.getElementById('prediction-algorithm');
|
||
predAlgoSelect.innerHTML = '<option value="">-- 选择算法 --</option>' +
|
||
algorithms.map(a => `<option value="${a.id}">${a.name}</option>`).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 showBatchAddMatchModal() {
|
||
document.getElementById('batch-match-data').value = '';
|
||
document.getElementById('batch-match-round').value = '';
|
||
document.getElementById('batch-match-modal').style.display = 'block';
|
||
}
|
||
|
||
// 批量添加比赛
|
||
async function batchAddMatches() {
|
||
const dataText = document.getElementById('batch-match-data').value.trim();
|
||
const defaultRound = document.getElementById('batch-match-round').value;
|
||
|
||
if (!dataText) {
|
||
alert('请输入比赛数据');
|
||
return;
|
||
}
|
||
|
||
const lines = dataText.split('\n').filter(line => line.trim());
|
||
let successCount = 0;
|
||
let failCount = 0;
|
||
let errors = [];
|
||
for (const line of lines) {
|
||
const parts = line.split(',').map(p => p.trim());
|
||
|
||
if (parts.length < 3) {
|
||
failCount++;
|
||
errors.push(`格式错误: ${line}`);
|
||
continue;
|
||
}
|
||
|
||
const matchData = {
|
||
match_code: parts[0],
|
||
home_team: parts[1],
|
||
away_team: parts[2],
|
||
match_time: parts[3] || null,
|
||
round_name: parts[4] || defaultRound || '小组赛',
|
||
status: 'upcoming'
|
||
};
|
||
|
||
try {
|
||
const response = await fetch(`${API_BASE}/api/matches`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(matchData)
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
successCount++;
|
||
} else {
|
||
failCount++;
|
||
errors.push(`${matchData.match_code}: ${result.message}`);
|
||
}
|
||
} catch (error) {
|
||
failCount++;
|
||
errors.push(`${matchData.match_code}: 网络错误`);
|
||
}
|
||
}
|
||
|
||
let message = `批量添加完成\n成功: ${successCount} 条\n失败: ${failCount} 条`;
|
||
if (errors.length > 0 && errors.length <= 5) {
|
||
message += '\n\n失败详情:\n' + errors.join('\n');
|
||
} else if (errors.length > 5) {
|
||
message += '\n\n失败详情(前5条):\n' + errors.slice(0, 5).join('\n') + '\n...';
|
||
}
|
||
|
||
alert(message);
|
||
|
||
if (successCount > 0) {
|
||
closeModal('batch-match-modal');
|
||
loadMatches();
|
||
loadPredictions();
|
||
}
|
||
}
|
||
|
||
// 显示添加比赛弹窗
|
||
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';
|
||
}
|
||
}); |