| {% extends 'base.html' %} |
|
|
| {% block content %} |
| <section class="page-grid admin-page-grid admin-activity-grid"> |
| <article class="glass-card form-panel wide-panel"> |
| <div class="section-head"> |
| <div> |
| <p class="eyebrow">Create Activity</p> |
| <h3>发布活动与打卡任务</h3> |
| </div> |
| </div> |
| <form method="post" action="/admin/activities" class="form-stack" id="activity-form"> |
| <div class="form-grid cols-2"> |
| <label> |
| <span>活动标题</span> |
| <input type="text" name="title" required /> |
| </label> |
| <label> |
| <span>线索发布时间间隔(分钟)</span> |
| <input type="number" name="clue_interval_minutes" min="0" placeholder="留空表示与活动开始同步" /> |
| </label> |
| <label> |
| <span>开始时间</span> |
| <input type="datetime-local" name="start_at" required /> |
| </label> |
| <label> |
| <span>截止时间</span> |
| <input type="datetime-local" name="deadline_at" required /> |
| </label> |
| </div> |
| <label> |
| <span>活动说明</span> |
| <textarea name="description" rows="3" placeholder="介绍活动安排、打卡要求和注意事项"></textarea> |
| </label> |
| <label class="checkbox-row"> |
| <input type="checkbox" name="is_visible" checked /> |
| <span>允许用户查看该活动</span> |
| </label> |
| <label class="checkbox-row"> |
| <input type="checkbox" name="leaderboard_visible" checked /> |
| <span>允许用户查看实时排行榜</span> |
| </label> |
|
|
| <div class="section-head tight-head"> |
| <div> |
| <p class="eyebrow">Tasks</p> |
| <h3>任务卡片</h3> |
| </div> |
| <button class="btn btn-secondary" type="button" id="add-task-btn">新增任务卡片</button> |
| </div> |
|
|
| <div class="task-builder" id="task-builder"> |
| <article class="task-builder-card" data-task-template> |
| <div class="builder-title-row"> |
| <strong>任务 1</strong> |
| <button type="button" class="btn btn-ghost small-btn" data-remove-task>删除</button> |
| </div> |
| <div class="form-grid cols-2"> |
| <label> |
| <span>任务标题</span> |
| <input type="text" name="task_title" required /> |
| </label> |
| <label> |
| <span>主图图床链接</span> |
| <input type="url" name="task_image_url" placeholder="https://..." required /> |
| </label> |
| <label class="full-span"> |
| <span>任务描述</span> |
| <textarea name="task_description" rows="2"></textarea> |
| </label> |
| <label class="full-span"> |
| <span>线索图图床链接</span> |
| <input type="url" name="task_clue_image_url" placeholder="https://..." /> |
| </label> |
| </div> |
| </article> |
| </div> |
| <button class="btn btn-primary" type="submit">发布活动</button> |
| </form> |
| </article> |
|
|
| <article class="glass-card table-panel activity-catalog-panel"> |
| <div class="section-head activity-catalog-head"> |
| <div> |
| <p class="eyebrow">Published Activities</p> |
| <h3>已发布活动</h3> |
| <p class="mini-note"></p> |
| </div> |
| </div> |
| <div class="published-activity-list"> |
| {% for activity in activities %} |
| <article class="published-activity-card"> |
| <div class="published-activity-hero"> |
| <div class="published-activity-title-wrap"> |
| <p class="eyebrow">Activity {{ loop.index }}</p> |
| <h3 class="published-activity-title">{{ activity.title }}</h3> |
| </div> |
| <div class="published-activity-badges"> |
| <span class="status-badge {% if activity.is_visible %}status-approved{% else %}status-rejected{% endif %}"> |
| {{ '活动可见' if activity.is_visible else '活动隐藏' }} |
| </span> |
| <span class="status-badge {% if activity.leaderboard_visible %}status-approved{% else %}status-rejected{% endif %}"> |
| {{ '排行可见' if activity.leaderboard_visible else '排行隐藏' }} |
| </span> |
| </div> |
| </div> |
|
|
| <div class="published-activity-metrics"> |
| <div class="activity-metric-card"> |
| <span>开始时间</span> |
| <strong>{{ activity.start_at|datetime_local }}</strong> |
| </div> |
| <div class="activity-metric-card"> |
| <span>截止时间</span> |
| <strong>{{ activity.deadline_at|datetime_local }}</strong> |
| </div> |
| <div class="activity-metric-card"> |
| <span>任务数量</span> |
| <strong>{{ activity.tasks|length }} 个任务</strong> |
| </div> |
| <div class="activity-metric-card"> |
| <span>创建人</span> |
| <strong>{{ activity.created_by.display_name }}</strong> |
| </div> |
| <div class="activity-metric-card"> |
| <span>线索间隔</span> |
| <strong>{{ activity.clue_interval_minutes if activity.clue_interval_minutes is not none else '与活动开始同步' }}</strong> |
| </div> |
| </div> |
|
|
| <form method="post" action="/admin/activities/{{ activity.id }}/visibility" class="published-settings-form"> |
| <div class="published-toggle-grid"> |
| <label class="published-toggle-card"> |
| <div class="published-toggle-copy"> |
| <strong>活动对用户可见</strong> |
| <span>关闭后,用户端不会显示,也不能直接进入此活动。</span> |
| </div> |
| <input type="checkbox" name="is_visible" {% if activity.is_visible %}checked{% endif %} /> |
| </label> |
| <label class="published-toggle-card"> |
| <div class="published-toggle-copy"> |
| <strong>排行榜对用户可见</strong> |
| <span>管理员始终可见,只控制普通用户是否能看到排行榜。</span> |
| </div> |
| <input type="checkbox" name="leaderboard_visible" {% if activity.leaderboard_visible %}checked{% endif %} /> |
| </label> |
| </div> |
| <div class="published-action-row"> |
| <a class="btn btn-primary" href="/admin/activities/{{ activity.id }}/edit">编辑活动</a> |
| <button class="btn btn-secondary" type="submit">保存显示设置</button> |
| </div> |
| </form> |
|
|
| <form method="post" action="/admin/activities/{{ activity.id }}/delete" class="published-delete-form"> |
| <button class="btn btn-danger" type="submit" onclick="return confirm('确定删除这个活动吗?相关任务、审核记录和本地提交图片都会一并删除。');">删除活动</button> |
| </form> |
| </article> |
| {% else %} |
| <article class="empty-state"> |
| <h3>还没有活动</h3> |
| <p>先在上方发布一个活动,这里会自动整理成清晰的管理卡片。</p> |
| </article> |
| {% endfor %} |
| </div> |
| </article> |
| </section> |
|
|
| <script> |
| (() => { |
| const builder = document.getElementById('task-builder'); |
| const addBtn = document.getElementById('add-task-btn'); |
| if (!builder || !addBtn) return; |
| |
| const renumber = () => { |
| builder.querySelectorAll('[data-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-task]'); |
| if (!removeBtn) return; |
| removeBtn.addEventListener('click', () => { |
| if (builder.querySelectorAll('[data-task-template]').length === 1) return; |
| card.remove(); |
| renumber(); |
| }); |
| }; |
| |
| builder.querySelectorAll('[data-task-template]').forEach(attachRemove); |
| |
| addBtn.addEventListener('click', () => { |
| const template = builder.querySelector('[data-task-template]'); |
| const clone = template.cloneNode(true); |
| clone.querySelectorAll('input, textarea').forEach((field) => { |
| if (field.type === 'checkbox') { |
| field.checked = false; |
| } else { |
| field.value = ''; |
| } |
| }); |
| attachRemove(clone); |
| builder.appendChild(clone); |
| renumber(); |
| }); |
| })(); |
| </script> |
| {% endblock %} |
|
|