| {% extends "base.html" %} |
|
|
| {% block title %}管理员后台{% endblock %} |
|
|
| {% block header_actions %} |
| <span class="status-pill status-live">{{ admin.role }} · {{ admin.username }}</span> |
| <form method="post" action="{{ url_for('admin_logout') }}"> |
| <button type="submit" class="button button-ghost">退出后台</button> |
| </form> |
| {% endblock %} |
|
|
| {% block content %} |
| <section class="hero-card"> |
| <div class="hero-copy-block"> |
| <div class="eyebrow">管理员后台</div> |
| <h1>统一管理用户、并发调度与全局实时日志。</h1> |
| <p class="hero-copy"> |
| 用户提交的课程配置会集中展示在这里。管理员可以手动录入用户信息、代为启动任务,并根据 Hugging Face Space 资源调整并行数。 |
| </p> |
| </div> |
| <div class="stat-grid admin-stat-grid"> |
| <div class="stat-card"> |
| <span>用户数</span> |
| <strong id="admin-users">{{ summary.users }}</strong> |
| </div> |
| <div class="stat-card"> |
| <span>运行中任务</span> |
| <strong id="admin-running">{{ summary.running_tasks }}</strong> |
| </div> |
| <div class="stat-card"> |
| <span>排队任务</span> |
| <strong id="admin-queued">{{ summary.queued_tasks }}</strong> |
| </div> |
| <div class="stat-card"> |
| <span>并行数</span> |
| <strong id="admin-parallelism">{{ summary.parallelism }}</strong> |
| </div> |
| </div> |
| </section> |
|
|
| <section class="content-grid content-grid-admin"> |
| <article class="panel"> |
| <div class="panel-head"> |
| <div> |
| <div class="section-kicker">系统设置</div> |
| <h2>并行调度</h2> |
| </div> |
| <span class="status-pill status-pending">资源敏感</span> |
| </div> |
| <form method="post" action="{{ url_for('update_parallelism') }}" class="form-grid form-grid-inline"> |
| <label class="field"> |
| <span>最大并行任务数</span> |
| <input type="number" name="parallelism" min="1" max="8" value="{{ summary.parallelism }}" required> |
| </label> |
| <button type="submit" class="button button-primary">保存并行数</button> |
| </form> |
| <p class="muted-note">建议根据 Space 资源从 1 开始逐步增加。每个任务都会独立启动一个无头浏览器实例。</p> |
| </article> |
|
|
| <article class="panel"> |
| <div class="panel-head"> |
| <div> |
| <div class="section-kicker">用户录入</div> |
| <h2>手动添加或更新用户</h2> |
| </div> |
| <span class="status-pill status-live">管理员可写</span> |
| </div> |
| <form method="post" action="{{ url_for('create_or_update_user_from_admin') }}" class="form-grid"> |
| <label class="field"> |
| <span>学号</span> |
| <input type="text" name="student_id" inputmode="numeric" placeholder="学生学号" required> |
| </label> |
| <label class="field"> |
| <span>密码</span> |
| <input type="password" name="password" placeholder="学生密码" required> |
| </label> |
| <label class="field"> |
| <span>初始课程号</span> |
| <input type="text" name="course_id" inputmode="numeric" placeholder="可选"> |
| </label> |
| <label class="field"> |
| <span>初始课序号</span> |
| <input type="text" name="course_index" inputmode="numeric" maxlength="2" placeholder="可选"> |
| </label> |
| <button type="submit" class="button button-primary">保存用户</button> |
| </form> |
| </article> |
|
|
| {% if admin.role == "superadmin" %} |
| <article class="panel"> |
| <div class="panel-head"> |
| <div> |
| <div class="section-kicker">权限管理</div> |
| <h2>创建普通管理员</h2> |
| </div> |
| <span class="status-pill status-live">仅超级管理员</span> |
| </div> |
| <form method="post" action="{{ url_for('create_admin_account') }}" class="form-grid form-grid-inline"> |
| <label class="field"> |
| <span>管理员账号</span> |
| <input type="text" name="username" placeholder="admin-ops" required> |
| </label> |
| <label class="field"> |
| <span>管理员密码</span> |
| <input type="password" name="password" placeholder="管理员密码" required> |
| </label> |
| <button type="submit" class="button button-ghost">创建管理员</button> |
| </form> |
| <div class="chip-wrap"> |
| {% for item in admins %} |
| <span class="feature-pill">{{ item.username }} · {{ item.role }}</span> |
| {% endfor %} |
| </div> |
| </article> |
| {% endif %} |
| </section> |
|
|
| <section class="panel"> |
| <div class="panel-head"> |
| <div> |
| <div class="section-kicker">用户列表</div> |
| <h2>多用户管理</h2> |
| </div> |
| <span class="status-pill status-live">管理员全量可见</span> |
| </div> |
|
|
| <div class="user-grid"> |
| {% if users %} |
| {% for item in users %} |
| <article class="user-card"> |
| <div class="user-card-head"> |
| <div> |
| <div class="user-code">{{ item.student_id }}</div> |
| <div class="user-meta-text">已选 {{ item.selected_count }}/{{ item.course_count }} · 最近登录 {{ item.last_login_at or "尚未登录" }}</div> |
| </div> |
| <span class="status-pill status-{{ item.active_task_status or 'idle' }}">{{ item.active_task_status or "idle" }}</span> |
| </div> |
|
|
| <div class="button-row compact"> |
| <form method="post" action="{{ url_for('start_user_task_from_admin', user_id=item.id) }}"> |
| <button type="submit" class="button button-primary button-small">启动</button> |
| </form> |
| <form method="post" action="{{ url_for('stop_user_task_from_admin', user_id=item.id) }}"> |
| <button type="submit" class="button button-secondary button-small">停止</button> |
| </form> |
| </div> |
|
|
| <form method="post" action="{{ url_for('update_user_password_from_admin', user_id=item.id) }}" class="form-grid"> |
| <label class="field"> |
| <span>重置密码</span> |
| <input type="password" name="password" placeholder="输入新密码" required> |
| </label> |
| <button type="submit" class="button button-ghost button-small">更新</button> |
| </form> |
|
|
| <form method="post" action="{{ url_for('add_user_course_from_admin', user_id=item.id) }}" class="form-grid form-grid-inline"> |
| <label class="field"> |
| <span>课程号</span> |
| <input type="text" name="course_id" inputmode="numeric" placeholder="课程号" required> |
| </label> |
| <label class="field"> |
| <span>课序号</span> |
| <input type="text" name="course_index" inputmode="numeric" maxlength="2" placeholder="01" required> |
| </label> |
| <button type="submit" class="button button-primary button-small">新增课程</button> |
| </form> |
|
|
| <div class="course-chip-grid"> |
| {% if item.courses %} |
| {% for course in item.courses %} |
| <div class="course-chip course-chip-{{ course.status }}"> |
| <div> |
| <strong>{{ course.course_id }}_{{ course.course_index }}</strong> |
| <p>{{ course.last_result or "等待执行" }}</p> |
| </div> |
| <form method="post" action="{{ url_for('delete_user_course_from_admin', user_id=item.id, course_record_id=course.id) }}"> |
| <button type="submit" class="button button-danger button-small">删</button> |
| </form> |
| </div> |
| {% endfor %} |
| {% else %} |
| <div class="empty-state small">这个用户还没有课程记录。</div> |
| {% endif %} |
| </div> |
| </article> |
| {% endfor %} |
| {% else %} |
| <div class="empty-state">当前还没有任何用户,先在上方录入用户信息。</div> |
| {% endif %} |
| </div> |
| </section> |
|
|
| <section class="panel console-panel"> |
| <div class="panel-head"> |
| <div> |
| <div class="section-kicker">全局日志</div> |
| <h2>管理员实时控制台</h2> |
| </div> |
| <span class="status-pill status-live">Live</span> |
| </div> |
| <div |
| class="log-console" |
| id="admin-log-stream" |
| data-last-id="{{ logs[-1].id if logs else 0 }}" |
| data-empty="当前还没有系统日志。" |
| > |
| {% if logs %} |
| {% for log in logs %} |
| <div class="log-line log-{{ log.level|lower }}"> |
| <span class="log-time">{{ log.created_at }}</span> |
| <span class="log-actor">{{ log.student_id or log.actor }}</span> |
| <span class="log-message">{{ log.message }}</span> |
| </div> |
| {% endfor %} |
| {% else %} |
| <div class="log-placeholder">当前还没有系统日志。</div> |
| {% endif %} |
| </div> |
| </section> |
|
|
| <script> |
| window.addEventListener("DOMContentLoaded", () => { |
| SCUApp.initLogStream("admin-log-stream", "{{ url_for('admin_log_stream') }}"); |
| SCUApp.initAdminStatus("{{ url_for('admin_status') }}"); |
| }); |
| </script> |
| {% endblock %} |
|
|