File size: 11,216 Bytes
3d06737 3d6b7f2 3d06737 4fb3744 3d06737 3d6b7f2 3d06737 4fb3744 3d06737 02a8414 4fb3744 02a8414 3d06737 3d6b7f2 3d06737 3d6b7f2 3d06737 3d6b7f2 3d06737 3d6b7f2 3d06737 3d6b7f2 3d06737 3d6b7f2 3d06737 4fb3744 3d06737 26d04d9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 | {% 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 %}
|