2.0.0 Alpha: Data Refinery
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}比赛导入 - YRTV{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-6 px-4 sm:px-0">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="space-y-2">
|
||||
{% for category, message in messages %}
|
||||
<div class="rounded-lg px-4 py-3 text-sm {% if category == 'success' %}bg-emerald-100 text-emerald-800{% elif category == 'warning' %}bg-amber-100 text-amber-800{% else %}bg-red-100 text-red-800{% endif %}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-slate-900 dark:text-white">比赛数据导入</h1>
|
||||
<p class="mt-1 text-sm text-slate-500 dark:text-slate-400">
|
||||
上传完整的 iframe_network.json,系统会自动识别比赛 ID,并执行 L1 → L2 → L3。
|
||||
</p>
|
||||
</div>
|
||||
<a href="{{ url_for('admin.dashboard') }}" class="text-sm font-medium text-yrtv-600 hover:text-yrtv-500">
|
||||
返回管理后台
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
<div class="rounded-xl bg-white p-6 shadow dark:bg-slate-800">
|
||||
<h2 class="text-lg font-semibold text-slate-900 dark:text-white">上传抓包</h2>
|
||||
<form method="POST" enctype="multipart/form-data" class="mt-5 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
iframe_network.json
|
||||
</label>
|
||||
<input type="file" name="capture" accept=".json,application/json" required
|
||||
class="mt-2 block w-full text-sm text-slate-600 file:mr-4 file:rounded-lg file:border-0 file:bg-yrtv-50 file:px-4 file:py-2 file:font-medium file:text-yrtv-700 hover:file:bg-yrtv-100 dark:text-slate-300">
|
||||
</div>
|
||||
<label class="flex items-start gap-2 text-sm text-slate-600 dark:text-slate-300">
|
||||
<input type="checkbox" name="replace" value="1" class="mt-1 rounded border-slate-300 text-yrtv-600">
|
||||
<span>允许替换已存在但内容不同的比赛。流水线失败时会自动恢复数据库。</span>
|
||||
</label>
|
||||
<button type="submit"
|
||||
class="w-full rounded-lg bg-yrtv-600 px-4 py-2.5 font-medium text-white hover:bg-yrtv-500">
|
||||
校验并开始导入
|
||||
</button>
|
||||
</form>
|
||||
<div class="mt-5 rounded-lg bg-amber-50 p-3 text-xs text-amber-800 dark:bg-amber-900/30 dark:text-amber-200">
|
||||
直接重复上传会被拒绝。每次流水线运行前都会备份 L1、L2、L3,并执行后置完整性检查。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl bg-white p-6 shadow dark:bg-slate-800 lg:col-span-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-slate-900 dark:text-white">作业状态</h2>
|
||||
{% if selected_job %}
|
||||
<span id="job-status" class="rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold uppercase text-slate-700 dark:bg-slate-700 dark:text-slate-200">
|
||||
{{ selected_job.status }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if selected_job %}
|
||||
<div class="mt-5">
|
||||
<div class="flex justify-between text-sm text-slate-600 dark:text-slate-300">
|
||||
<span id="job-stage">{{ selected_job.current_stage or 'queued' }}</span>
|
||||
<span id="job-progress-label">{{ selected_job.progress }}%</span>
|
||||
</div>
|
||||
<div class="mt-2 h-2 overflow-hidden rounded-full bg-slate-200 dark:bg-slate-700">
|
||||
<div id="job-progress" class="h-full bg-yrtv-500 transition-all" style="width: {{ selected_job.progress }}%"></div>
|
||||
</div>
|
||||
<p id="job-message" class="mt-3 text-sm text-slate-600 dark:text-slate-300">
|
||||
{{ selected_job.message or '等待执行' }}
|
||||
</p>
|
||||
<pre id="job-log" class="mt-4 max-h-96 overflow-auto whitespace-pre-wrap rounded-lg bg-slate-950 p-4 text-xs text-slate-200">{{ selected_job.log_text }}</pre>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mt-8 rounded-lg border border-dashed border-slate-300 p-8 text-center text-sm text-slate-500 dark:border-slate-600">
|
||||
上传比赛或从下方选择历史作业。
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded-xl bg-white shadow dark:bg-slate-800">
|
||||
<div class="border-b border-slate-200 px-5 py-4 dark:border-slate-700">
|
||||
<h2 class="font-semibold text-slate-900 dark:text-white">最近作业</h2>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-slate-200 dark:divide-slate-700">
|
||||
<thead class="bg-slate-50 dark:bg-slate-900/50">
|
||||
<tr>
|
||||
<th class="px-5 py-3 text-left text-xs font-semibold uppercase text-slate-500">ID</th>
|
||||
<th class="px-5 py-3 text-left text-xs font-semibold uppercase text-slate-500">类型</th>
|
||||
<th class="px-5 py-3 text-left text-xs font-semibold uppercase text-slate-500">比赛</th>
|
||||
<th class="px-5 py-3 text-left text-xs font-semibold uppercase text-slate-500">状态</th>
|
||||
<th class="px-5 py-3 text-left text-xs font-semibold uppercase text-slate-500">阶段</th>
|
||||
<th class="px-5 py-3 text-left text-xs font-semibold uppercase text-slate-500">耗时</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100 dark:divide-slate-700">
|
||||
{% for job in jobs %}
|
||||
<tr class="hover:bg-slate-50 dark:hover:bg-slate-700/40">
|
||||
<td class="px-5 py-3 text-sm">
|
||||
<a href="{{ url_for('admin.import_match', job_id=job.id) }}" class="font-medium text-yrtv-600">#{{ job.id }}</a>
|
||||
</td>
|
||||
<td class="px-5 py-3 text-sm text-slate-600 dark:text-slate-300">{{ job.job_type }}</td>
|
||||
<td class="px-5 py-3 text-sm font-mono text-slate-600 dark:text-slate-300">{{ job.match_id or '-' }}</td>
|
||||
<td class="px-5 py-3 text-sm text-slate-600 dark:text-slate-300">{{ job.status }}</td>
|
||||
<td class="px-5 py-3 text-sm text-slate-600 dark:text-slate-300">{{ job.current_stage or '-' }}</td>
|
||||
<td class="px-5 py-3 text-sm text-slate-600 dark:text-slate-300">{{ '%.2fs'|format(job.duration_seconds) if job.duration_seconds is not none else '-' }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="6" class="px-5 py-8 text-center text-sm text-slate-500">暂无作业</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% if selected_job %}
|
||||
<script>
|
||||
const jobId = {{ selected_job.id }};
|
||||
let pollTimer = null;
|
||||
|
||||
async function refreshJob() {
|
||||
const response = await fetch(`/admin/api/jobs/${jobId}`);
|
||||
if (!response.ok) return;
|
||||
const job = await response.json();
|
||||
document.getElementById('job-status').textContent = job.status;
|
||||
document.getElementById('job-stage').textContent = job.current_stage || job.status;
|
||||
document.getElementById('job-progress-label').textContent = `${job.progress}%`;
|
||||
document.getElementById('job-progress').style.width = `${job.progress}%`;
|
||||
document.getElementById('job-message').textContent = job.message || '';
|
||||
const log = document.getElementById('job-log');
|
||||
log.textContent = job.log_text || '';
|
||||
log.scrollTop = log.scrollHeight;
|
||||
if (job.status === 'succeeded' || job.status === 'failed') {
|
||||
clearInterval(pollTimer);
|
||||
}
|
||||
}
|
||||
|
||||
pollTimer = setInterval(refreshJob, 1500);
|
||||
refreshJob();
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user