Spaces:
Paused
Paused
| {% extends "base.html" %} | |
| {% block title %}管理后台 | SCU 选课控制台{% endblock %} | |
| {% block body_class %}admin-theme{% endblock %} | |
| {% block content %} | |
| <section class="dashboard-shell admin-dashboard" data-log-stream-url="{{ url_for('stream_admin_logs', last_id=recent_logs[-1].id if recent_logs else 0) }}" data-status-url="{{ url_for('admin_status') }}"> | |
| <header class="topbar reveal-up"> | |
| <div> | |
| <span class="eyebrow">Admin Console</span> | |
| <h1>管理员后台</h1> | |
| <p>当前管理员:{{ admin_identity.username }}{% if is_super_admin %} · 超级管理员{% endif %}</p> | |
| </div> | |
| <form method="post" action="{{ url_for('admin_logout') }}"> | |
| <button type="submit" class="btn btn-ghost">退出后台</button> | |
| </form> | |
| </header> | |
| <section class="metric-grid reveal-up delay-1"> | |
| <article class="metric-card"> | |
| <span>用户数</span> | |
| <strong id="stat-users">{{ stats.users_count }}</strong> | |
| <small>已录入的学生账号</small> | |
| </article> | |
| <article class="metric-card"> | |
| <span>运行中任务</span> | |
| <strong id="stat-running">{{ stats.running_count }}</strong> | |
| <small>排队中:<span id="stat-pending">{{ stats.pending_count }}</span></small> | |
| </article> | |
| <article class="metric-card"> | |
| <span>总课程目标</span> | |
| <strong>{{ stats.courses_count }}</strong> | |
| <small>管理员可见全部课程号与课序号</small> | |
| </article> | |
| <article class="metric-card"> | |
| <span>管理员总数</span> | |
| <strong>{{ stats.admins_count }}</strong> | |
| <small>包含 1 位超级管理员</small> | |
| </article> | |
| </section> | |
| <section class="content-grid admin-grid"> | |
| <article class="card reveal-up delay-2"> | |
| <div class="card-head"> | |
| <span class="kicker">调度设置</span> | |
| <h2>并行数</h2> | |
| <p>建议根据 Hugging Face Space 的 CPU 与内存情况控制在较低范围。</p> | |
| </div> | |
| <form method="post" action="{{ url_for('update_parallel_limit') }}" class="form-grid form-grid-compact"> | |
| <label class="field"> | |
| <span>当前并行数</span> | |
| <input type="number" id="parallel-limit-input" name="parallel_limit" min="1" max="8" value="{{ parallel_limit }}" required> | |
| </label> | |
| <button type="submit" class="btn btn-primary">更新并行数</button> | |
| </form> | |
| </article> | |
| <article class="card reveal-up delay-2"> | |
| <div class="card-head"> | |
| <span class="kicker">新增用户</span> | |
| <h2>手动录入用户信息</h2> | |
| <p>管理员可以直接创建学生账号,普通用户随后即可用学号和密码登录。</p> | |
| </div> | |
| <form method="post" action="{{ url_for('create_user') }}" class="form-grid form-grid-compact"> | |
| <label class="field"> | |
| <span>学号</span> | |
| <input type="text" name="student_id" inputmode="numeric" placeholder="13 位学号" required> | |
| </label> | |
| <label class="field"> | |
| <span>显示名称</span> | |
| <input type="text" name="display_name" placeholder="可选备注"> | |
| </label> | |
| <label class="field span-2"> | |
| <span>密码</span> | |
| <input type="password" name="password" placeholder="教务系统密码" required> | |
| </label> | |
| <button type="submit" class="btn btn-secondary">创建用户</button> | |
| </form> | |
| </article> | |
| {% if is_super_admin %} | |
| <article class="card reveal-up delay-2"> | |
| <div class="card-head"> | |
| <span class="kicker">管理员管理</span> | |
| <h2>新增管理员</h2> | |
| <p>只有超级管理员可以继续创建普通管理员。</p> | |
| </div> | |
| <form method="post" action="{{ url_for('create_admin') }}" class="form-grid form-grid-compact"> | |
| <label class="field"> | |
| <span>管理员账号</span> | |
| <input type="text" name="username" placeholder="输入管理员账号" required> | |
| </label> | |
| <label class="field"> | |
| <span>管理员密码</span> | |
| <input type="password" name="password" placeholder="输入管理员密码" required> | |
| </label> | |
| <button type="submit" class="btn btn-ghost">创建管理员</button> | |
| </form> | |
| <div class="chip-row"> | |
| <span class="chip highlight">超级管理员:{{ admin_identity.username }}</span> | |
| {% for admin in admins %} | |
| <span class="chip">{{ admin.username }}</span> | |
| {% endfor %} | |
| </div> | |
| </article> | |
| {% endif %} | |
| <article class="card reveal-up delay-3 span-2"> | |
| <div class="card-head split"> | |
| <div> | |
| <span class="kicker">任务总览</span> | |
| <h2>最近任务</h2> | |
| <p>用于快速确认任务是否正在排队、执行、停止或失败。</p> | |
| </div> | |
| <span class="status-pill status-running">实时刷新</span> | |
| </div> | |
| <div class="course-table-wrap"> | |
| <table class="data-table"> | |
| <thead> | |
| <tr> | |
| <th>任务</th> | |
| <th>学号</th> | |
| <th>状态</th> | |
| <th>触发者</th> | |
| <th>更新时间</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {% if recent_tasks %} | |
| {% for task in recent_tasks %} | |
| <tr> | |
| <td>#{{ task.id }}</td> | |
| <td>{{ task.student_id }}</td> | |
| <td><span class="status-pill status-{{ task.status }}">{{ task_labels.get(task.status, task.status) }}</span></td> | |
| <td>{{ task.requested_by_role }}:{{ task.requested_by }}</td> | |
| <td>{{ task.updated_at }}</td> | |
| </tr> | |
| {% endfor %} | |
| {% else %} | |
| <tr> | |
| <td colspan="5" class="empty-cell">还没有任务记录。</td> | |
| </tr> | |
| {% endif %} | |
| </tbody> | |
| </table> | |
| </div> | |
| </article> | |
| <article class="card reveal-up delay-3 span-2"> | |
| <div class="card-head split"> | |
| <div> | |
| <span class="kicker">全局日志</span> | |
| <h2>所有用户的运行日志</h2> | |
| <p>日志会持续流入,便于管理员确认浏览器登录、查课、提交结果与错误信息。</p> | |
| </div> | |
| <span class="live-dot">LIVE</span> | |
| </div> | |
| <div class="log-console" id="log-console"> | |
| {% if recent_logs %} | |
| {% for log in recent_logs %} | |
| <div class="log-line level-{{ log.level|lower }}"> | |
| <span class="log-meta">{{ log.created_at }} · {{ log.student_id or 'system' }} · {{ log.scope }} · {{ log.level }}</span> | |
| <span>{{ log.message }}</span> | |
| </div> | |
| {% endfor %} | |
| {% else %} | |
| <div class="log-line level-info muted">暂无日志,用户启动任务后这里会自动刷新。</div> | |
| {% endif %} | |
| </div> | |
| </article> | |
| <article class="card reveal-up delay-4 span-2"> | |
| <div class="card-head"> | |
| <span class="kicker">用户清单</span> | |
| <h2>所有用户与课程详情</h2> | |
| <p>可以直接修改用户信息、增减课程,或代替用户启动和停止任务。</p> | |
| </div> | |
| <div class="user-card-grid"> | |
| {% for user in users %} | |
| <section class="user-card"> | |
| <div class="user-card-head"> | |
| <div> | |
| <h3>{{ user.display_name or user.student_id }}</h3> | |
| <p>{{ user.student_id }}</p> | |
| </div> | |
| <span class="status-pill status-{{ user.latest_task.status if user.latest_task else 'idle' }}"> | |
| {{ task_labels.get(user.latest_task.status, '未启动') if user.latest_task else '未启动' }} | |
| </span> | |
| </div> | |
| <div class="chip-row tight"> | |
| <span class="chip {% if user.is_active %}highlight{% endif %}">{{ '启用中' if user.is_active else '已禁用' }}</span> | |
| <span class="chip">课程 {{ user.course_count }}</span> | |
| <span class="chip">最近任务 {{ user.latest_task.id if user.latest_task else '--' }}</span> | |
| </div> | |
| <form method="post" action="{{ url_for('update_user', user_id=user.id) }}" class="form-grid form-grid-compact slim-form"> | |
| <label class="field span-2"> | |
| <span>显示名称</span> | |
| <input type="text" name="display_name" value="{{ user.display_name }}" placeholder="备注名称"> | |
| </label> | |
| <label class="field span-2"> | |
| <span>重置密码</span> | |
| <input type="password" name="password" placeholder="留空表示不修改"> | |
| </label> | |
| <button type="submit" class="btn btn-ghost">保存用户</button> | |
| </form> | |
| <div class="button-row wrap-row"> | |
| <form method="post" action="{{ url_for('toggle_user', user_id=user.id) }}"> | |
| <button type="submit" class="btn btn-ghost {% if not user.is_active %}danger{% endif %}">{{ '禁用' if user.is_active else '启用' }}</button> | |
| </form> | |
| <form method="post" action="{{ url_for('admin_start_user_task', user_id=user.id) }}"> | |
| <button type="submit" class="btn btn-primary">代启动任务</button> | |
| </form> | |
| <form method="post" action="{{ url_for('admin_stop_user_task', user_id=user.id) }}"> | |
| <button type="submit" class="btn btn-ghost danger">代停止任务</button> | |
| </form> | |
| </div> | |
| <form method="post" action="{{ url_for('admin_add_course', user_id=user.id) }}" class="form-grid form-grid-compact slim-form"> | |
| <label class="field"> | |
| <span>类型</span> | |
| <select name="category"> | |
| <option value="free">自由选课</option> | |
| <option value="plan">方案选课</option> | |
| </select> | |
| </label> | |
| <label class="field"> | |
| <span>课程号</span> | |
| <input type="text" name="course_id" placeholder="课程号" required> | |
| </label> | |
| <label class="field"> | |
| <span>课序号</span> | |
| <input type="text" name="course_index" placeholder="01" maxlength="2" required> | |
| </label> | |
| <button type="submit" class="btn btn-secondary">为该用户加课</button> | |
| </form> | |
| <div class="course-list"> | |
| {% if user.courses %} | |
| {% for course in user.courses %} | |
| <div class="course-chip-row"> | |
| <span>{{ category_labels.get(course.category, course.category) }} · {{ course.course_id }}_{{ course.course_index }}</span> | |
| <form method="post" action="{{ url_for('admin_delete_course', course_target_id=course.id) }}"> | |
| <button type="submit" class="inline-action">删除</button> | |
| </form> | |
| </div> | |
| {% endfor %} | |
| {% else %} | |
| <div class="empty-mini">当前没有课程目标。</div> | |
| {% endif %} | |
| </div> | |
| </section> | |
| {% else %} | |
| <div class="empty-state-card"> | |
| 还没有录入任何用户,请先通过上方表单创建。 | |
| </div> | |
| {% endfor %} | |
| </div> | |
| </article> | |
| </section> | |
| </section> | |
| {% endblock %} | |