| {% extends 'base.html' %} |
|
|
| {% block content %} |
| <section class="hero-card admin-hero"> |
| <div> |
| <a class="ghost-link" href="/admin/activities">返回活动管理</a> |
| <p class="eyebrow">Edit Activity</p> |
| <h2>{{ activity.title }}</h2> |
| <p class="lead">在这里可以修改活动时间、排行榜可见性、任务内容和图床链接,并删除不再需要的任务。管理员任务图片仅保留链接方式,用户提交图片仍保存在 Docker 本地目录。</p> |
| </div> |
| <div class="hero-badges"> |
| <span class="pill">创建人 {{ activity.created_by.display_name }}</span> |
| <span class="pill">{{ activity.tasks|length }} 个任务</span> |
| <span class="pill">开始 {{ activity.start_at|datetime_local }}</span> |
| <span class="pill">{{ '用户端可见' if activity.is_visible else '用户端隐藏' }}</span> |
| </div> |
| <div class="card-footer"> |
| <button class="btn btn-danger" type="submit" form="delete-activity-form" onclick="return confirm('确定删除这个活动吗?相关任务、审核记录和本地提交图片都会一并删除。');">删除整个活动</button> |
| </div> |
| </section> |
|
|
| <form method="post" action="/admin/activities/{{ activity.id }}/edit" class="form-stack"> |
| <section class="glass-card form-panel wide-panel"> |
| <div class="section-head"> |
| <div> |
| <p class="eyebrow">Activity Settings</p> |
| <h3>活动信息</h3> |
| </div> |
| </div> |
| <div class="form-grid cols-2"> |
| <label> |
| <span>活动标题</span> |
| <input type="text" name="title" value="{{ activity.title }}" required /> |
| </label> |
| <label> |
| <span>线索发布时间间隔(分钟)</span> |
| <input type="number" name="clue_interval_minutes" min="0" value="{{ activity.clue_interval_minutes if activity.clue_interval_minutes is not none else '' }}" placeholder="留空表示与活动开始同步" /> |
| </label> |
| <label> |
| <span>开始时间</span> |
| <input type="datetime-local" name="start_at" value="{{ activity.start_at.strftime('%Y-%m-%dT%H:%M') }}" required /> |
| </label> |
| <label> |
| <span>截止时间</span> |
| <input type="datetime-local" name="deadline_at" value="{{ activity.deadline_at.strftime('%Y-%m-%dT%H:%M') }}" required /> |
| </label> |
| </div> |
| <label> |
| <span>活动说明</span> |
| <textarea name="description" rows="3">{{ activity.description or '' }}</textarea> |
| </label> |
| <label class="checkbox-row"> |
| <input type="checkbox" name="is_visible" {% if activity.is_visible %}checked{% endif %} /> |
| <span>允许用户查看该活动</span> |
| </label> |
| <label class="checkbox-row"> |
| <input type="checkbox" name="leaderboard_visible" {% if activity.leaderboard_visible %}checked{% endif %} /> |
| <span>允许用户查看实时排行榜</span> |
| </label> |
| </section> |
|
|
| <section class="glass-card leaderboard-card wide-panel"> |
| <div class="section-head compact-head"> |
| <div> |
| <p class="eyebrow">Live Ranking</p> |
| <h3>活动实时排行榜</h3> |
| <p class="mini-note">管理员始终可见,用于现场统筹和审核判断;用户是否可见由上方开关控制。</p> |
| </div> |
| <div class="chip-row"> |
| <span class="chip">活动{{ '可见' if activity.is_visible else '隐藏' }}</span> |
| <span class="chip">排行榜{{ '可见' if activity.leaderboard_visible else '隐藏' }}</span> |
| </div> |
| </div> |
| <div class="rank-table-wrap"> |
| <table class="rank-table"> |
| <thead> |
| <tr> |
| <th>排名</th> |
| <th>小组</th> |
| <th>完成</th> |
| <th>人数</th> |
| <th>总耗时</th> |
| </tr> |
| </thead> |
| <tbody> |
| {% for row in leaderboard %} |
| <tr> |
| <td>#{{ loop.index }}</td> |
| <td>{{ row.group_name }}</td> |
| <td>{{ row.completed_count }}</td> |
| <td>{{ row.member_count }}</td> |
| <td>{{ row.total_elapsed|duration_human }}</td> |
| </tr> |
| {% else %} |
| <tr> |
| <td colspan="5">当前还没有通过审核的小组打卡记录。</td> |
| </tr> |
| {% endfor %} |
| </tbody> |
| </table> |
| </div> |
| </section> |
|
|
| <section class="glass-card form-panel wide-panel"> |
| <div class="section-head"> |
| <div> |
| <p class="eyebrow">Existing Tasks</p> |
| <h3>已有任务</h3> |
| </div> |
| </div> |
| <div class="task-editor-list"> |
| {% for task in activity.tasks %} |
| <article class="task-builder-card task-editor-card"> |
| <div class="builder-title-row"> |
| <strong>任务 {{ loop.index }}</strong> |
| <button |
| class="btn btn-danger small-btn" |
| type="submit" |
| form="delete-task-{{ task.id }}" |
| onclick="return confirm('确定删除这个任务吗?相关提交记录也会一并删除。');" |
| >删除任务</button> |
| </div> |
|
|
| <input type="hidden" name="existing_task_id" value="{{ task.id }}" /> |
|
|
| <div class="form-grid cols-2"> |
| <label> |
| <span>任务标题</span> |
| <input type="text" name="existing_task_title" value="{{ task.title }}" required /> |
| </label> |
| <label> |
| <span>主图图床链接</span> |
| <input type="url" name="existing_task_image_url" value="{{ task.image_url or '' }}" placeholder="https://..." required /> |
| </label> |
| <label class="full-span"> |
| <span>任务描述</span> |
| <textarea name="existing_task_description" rows="2">{{ task.description or '' }}</textarea> |
| </label> |
| <label> |
| <span>线索图图床链接</span> |
| <input type="url" name="existing_task_clue_image_url" value="{{ task.clue_image_url or '' }}" placeholder="https://..." /> |
| </label> |
| <label class="checkbox-row align-end-checkbox"> |
| <input type="checkbox" name="existing_task_remove_clue" value="{{ task.id }}" /> |
| <span>移除当前线索图</span> |
| </label> |
| </div> |
|
|
| <div class="editor-preview-grid"> |
| <div> |
| <span class="mini-note">当前主图</span> |
| <img class="editor-thumb" src="{{ task.image_url if task.image_url else '/media/tasks/' ~ task.id ~ '/image' }}" alt="{{ task.title }} 主图" /> |
| </div> |
| <div> |
| <span class="mini-note">当前线索图</span> |
| {% if task.clue_image_url or task.clue_image_filename or task.clue_image_path %} |
| <img class="editor-thumb" src="{{ task.clue_image_url if task.clue_image_url else '/media/tasks/' ~ task.id ~ '/clue' }}" alt="{{ task.title }} 线索图" /> |
| {% else %} |
| <div class="empty-thumb">未设置线索图</div> |
| {% endif %} |
| </div> |
| </div> |
|
|
| <div class="chip-row"> |
| <span class="chip">提交记录 {{ task.submissions|length }} 条</span> |
| <span class="chip">当前顺序 {{ task.display_order }}</span> |
| <span class="chip">线索发布时间 {{ task.clue_release_at|datetime_local if task.clue_release_at else '未发布' }}</span> |
| </div> |
| </article> |
| {% endfor %} |
| </div> |
| </section> |
|
|
| <section class="glass-card form-panel wide-panel"> |
| <div class="section-head tight-head"> |
| <div> |
| <p class="eyebrow">New Tasks</p> |
| <h3>新增任务</h3> |
| </div> |
| <button class="btn btn-secondary" type="button" id="add-new-task-btn">新增任务卡片</button> |
| </div> |
|
|
| <div class="task-builder" id="new-task-builder"> |
| <article class="task-builder-card" data-new-task-template> |
| <div class="builder-title-row"> |
| <strong>新增任务 1</strong> |
| <button type="button" class="btn btn-ghost small-btn" data-remove-new-task>删除</button> |
| </div> |
| <div class="form-grid cols-2"> |
| <label> |
| <span>任务标题</span> |
| <input type="text" name="new_task_title" /> |
| </label> |
| <label> |
| <span>主图图床链接</span> |
| <input type="url" name="new_task_image_url" placeholder="https://..." /> |
| </label> |
| <label class="full-span"> |
| <span>任务描述</span> |
| <textarea name="new_task_description" rows="2"></textarea> |
| </label> |
| <label class="full-span"> |
| <span>线索图图床链接</span> |
| <input type="url" name="new_task_clue_image_url" placeholder="https://..." /> |
| </label> |
| </div> |
| </article> |
| </div> |
|
|
| <div class="card-footer"> |
| <span class="mini-note">保存后会自动重排任务顺序,并按新的顺序刷新线索发布时间。</span> |
| <button class="btn btn-primary" type="submit">保存活动修改</button> |
| </div> |
| </section> |
| </form> |
|
|
| <form id="delete-activity-form" method="post" action="/admin/activities/{{ activity.id }}/delete"></form> |
|
|
| {% for task in activity.tasks %} |
| <form id="delete-task-{{ task.id }}" method="post" action="/admin/tasks/{{ task.id }}/delete"></form> |
| {% endfor %} |
|
|
| <script> |
| (() => { |
| const builder = document.getElementById('new-task-builder'); |
| const addBtn = document.getElementById('add-new-task-btn'); |
| if (!builder || !addBtn) return; |
| |
| const renumber = () => { |
| builder.querySelectorAll('[data-new-task-template]').forEach((card, index) => { |
| const title = card.querySelector('.builder-title-row strong'); |
| if (title) title.textContent = `新增任务 ${index + 1}`; |
| }); |
| }; |
| |
| const attachRemove = (card) => { |
| const removeBtn = card.querySelector('[data-remove-new-task]'); |
| if (!removeBtn) return; |
| removeBtn.addEventListener('click', () => { |
| if (builder.querySelectorAll('[data-new-task-template]').length === 1) { |
| card.querySelectorAll('input, textarea').forEach((field) => { |
| field.value = ''; |
| }); |
| return; |
| } |
| card.remove(); |
| renumber(); |
| }); |
| }; |
| |
| builder.querySelectorAll('[data-new-task-template]').forEach(attachRemove); |
| |
| addBtn.addEventListener('click', () => { |
| const template = builder.querySelector('[data-new-task-template]'); |
| const clone = template.cloneNode(true); |
| clone.querySelectorAll('input, textarea').forEach((field) => { |
| field.value = ''; |
| }); |
| attachRemove(clone); |
| builder.appendChild(clone); |
| renumber(); |
| }); |
| })(); |
| </script> |
| {% endblock %} |
|
|
|
|
|
|
|
|
|
|
|
|