| {% extends 'base.html' %} |
|
|
| {% block content %} |
| <section class="glass-card form-panel wide-panel"> |
| <div class="section-head"> |
| <div> |
| <p class="eyebrow">Review Center</p> |
| <h3>实时审核中心</h3> |
| <p class="mini-note" id="review-live-note">待审核内容会自动分配给在线管理员,页面会持续刷新。</p> |
| </div> |
| <div class="hero-badges"> |
| <span class="pill" id="online-admin-pill">当前在线管理员 {{ online_admin_count }}</span> |
| <form id="download-form" method="post" action="/admin/reviews/download" class="inline-form"> |
| <button class="btn btn-primary" type="submit">下载已勾选图片</button> |
| </form> |
| </div> |
| </div> |
|
|
| <form method="get" action="/admin/reviews" class="form-grid cols-3 review-filter-form"> |
| <label> |
| <span>活动筛选</span> |
| <select name="activity_id"> |
| <option value="">全部活动</option> |
| {% for activity in activities %} |
| <option value="{{ activity.id }}" {% if activity_filter == activity.id ~ '' %}selected{% endif %}>{{ activity.title }}</option> |
| {% endfor %} |
| </select> |
| </label> |
| <label class="review-filter-action"> |
| <span> </span> |
| <button class="btn btn-secondary" type="submit">应用筛选</button> |
| </label> |
| </form> |
| </section> |
|
|
| <section class="page-grid admin-page-grid review-live-grid"> |
| <article class="glass-card table-panel"> |
| <div class="section-head"> |
| <div> |
| <p class="eyebrow">Assigned Queue</p> |
| <h3>分配给我的待审核任务</h3> |
| </div> |
| </div> |
| <div class="review-grid" id="assigned-review-grid"> |
| {% for submission in assigned_submissions %} |
| <article class="glass-card review-card" data-review-card data-submission-id="{{ submission.id }}"> |
| <div class="card-topline"> |
| <label class="checkbox-row compact-checkbox"> |
| <input type="checkbox" name="submission_ids" value="{{ submission.id }}" form="download-form" /> |
| <span>加入下载</span> |
| </label> |
| <span class="status-badge">待审核</span> |
| </div> |
| <h3>{{ submission.task.title }}</h3> |
| <p class="muted">{{ submission.task.activity.title }}</p> |
| <p class="muted">{{ submission.group.name if submission.group else '未分组' }} · 上传人 {{ submission.user.full_name if submission.user else '未知成员' }}</p> |
| <img class="review-image" src="/media/submissions/{{ submission.id }}" alt="{{ submission.task.title }}" /> |
| <p class="mini-note">提交时间:{{ submission.created_at|datetime_local }}</p> |
| <a class="btn btn-ghost full-width-btn" href="/media/submissions/{{ submission.id }}?download=1">单张下载</a> |
| <form method="post" action="/admin/submissions/{{ submission.id }}/review" class="form-stack compact-form review-action-form"> |
| <label> |
| <span>审核备注</span> |
| <textarea name="feedback" rows="2" placeholder="可填写通过说明或驳回原因"></textarea> |
| </label> |
| <div class="action-grid two-actions"> |
| <button class="btn btn-primary" type="submit" name="decision" value="approved">审核通过</button> |
| <button class="btn btn-danger" type="submit" name="decision" value="rejected">审核驳回</button> |
| </div> |
| </form> |
| </article> |
| {% else %} |
| <article class="empty-state" id="assigned-empty-state"> |
| <h3>当前还没有分配给你的待审核内容</h3> |
| <p>保持页面打开,系统会自动把新的待审核任务分配给在线管理员。</p> |
| </article> |
| {% endfor %} |
| </div> |
| </article> |
|
|
| <article class="glass-card table-panel"> |
| <div class="section-head"> |
| <div> |
| <p class="eyebrow">Recent Reviews</p> |
| <h3>最近审核结果</h3> |
| </div> |
| </div> |
| <div class="stack-list" id="recent-review-list"> |
| {% for submission in recent_submissions %} |
| <article class="stack-item stack-item-block"> |
| <div> |
| <strong>{{ submission.task.activity.title }} · {{ submission.task.title }}</strong> |
| <p class="muted">{{ submission.group.name if submission.group else '未分组' }} · {{ submission.user.full_name if submission.user else '未知成员' }}</p> |
| </div> |
| <div class="chip-row"> |
| <span class="status-badge {% if submission.status == 'approved' %}status-approved{% else %}status-rejected{% endif %}"> |
| {{ '通过' if submission.status == 'approved' else '驳回' }} |
| </span> |
| <span class="chip">{{ submission.reviewed_by.display_name if submission.reviewed_by else '未知管理员' }}</span> |
| <span class="chip">{{ submission.reviewed_at|datetime_local if submission.reviewed_at else '-' }}</span> |
| </div> |
| </article> |
| {% else %} |
| <p class="muted">还没有最近审核记录。</p> |
| {% endfor %} |
| </div> |
| </article> |
| </section> |
|
|
| <script> |
| (() => { |
| const assignedGrid = document.getElementById('assigned-review-grid'); |
| const recentList = document.getElementById('recent-review-list'); |
| const onlineAdminPill = document.getElementById('online-admin-pill'); |
| const activityFilter = '{{ activity_filter }}'; |
| const FEED_POLL_INTERVAL_MS = 5000; |
| let feedTimerId = null; |
| let feedInFlight = false; |
| const selectedIds = new Set( |
| Array.from(document.querySelectorAll('input[name="submission_ids"]:checked')).map((item) => item.value) |
| ); |
| |
| const syncSelection = (target) => { |
| if (!target || target.name !== 'submission_ids') return; |
| if (target.checked) { |
| selectedIds.add(target.value); |
| } else { |
| selectedIds.delete(target.value); |
| } |
| }; |
| |
| document.addEventListener('change', (event) => { |
| syncSelection(event.target); |
| }); |
| |
| const cardHtml = (item) => ` |
| <article class="glass-card review-card" data-review-card data-submission-id="${item.id}"> |
| <div class="card-topline"> |
| <label class="checkbox-row compact-checkbox"> |
| <input type="checkbox" name="submission_ids" value="${item.id}" form="download-form" ${selectedIds.has(String(item.id)) ? 'checked' : ''} /> |
| <span>加入下载</span> |
| </label> |
| <span class="status-badge">待审核</span> |
| </div> |
| <h3>${item.task_title}</h3> |
| <p class="muted">${item.activity_title}</p> |
| <p class="muted">${item.group_name} · 上传人 ${item.uploader_name}</p> |
| <img class="review-image" src="${item.image_url}" alt="${item.task_title}" /> |
| <p class="mini-note">提交时间:${item.created_at}</p> |
| <a class="btn btn-ghost full-width-btn" href="${item.download_url}">单张下载</a> |
| <form method="post" action="/admin/submissions/${item.id}/review" class="form-stack compact-form review-action-form"> |
| <label> |
| <span>审核备注</span> |
| <textarea name="feedback" rows="2" placeholder="可填写通过说明或驳回原因"></textarea> |
| </label> |
| <div class="action-grid two-actions"> |
| <button class="btn btn-primary" type="submit" name="decision" value="approved">审核通过</button> |
| <button class="btn btn-danger" type="submit" name="decision" value="rejected">审核驳回</button> |
| </div> |
| </form> |
| </article>`; |
| |
| const recentHtml = (item) => ` |
| <article class="stack-item stack-item-block"> |
| <div> |
| <strong>${item.activity_title} · ${item.task_title}</strong> |
| <p class="muted">${item.group_name} · ${item.uploader_name}</p> |
| </div> |
| <div class="chip-row"> |
| <span class="status-badge ${item.status === 'approved' ? 'status-approved' : 'status-rejected'}"> |
| ${item.status === 'approved' ? '通过' : '驳回'} |
| </span> |
| <span class="chip">${item.reviewed_by_name || '未知管理员'}</span> |
| <span class="chip">${item.reviewed_at || '-'}</span> |
| </div> |
| </article>`; |
| |
| const attachReviewForms = () => { |
| document.querySelectorAll('.review-action-form').forEach((form) => { |
| if (form.dataset.bound === 'true') return; |
| form.dataset.bound = 'true'; |
| form.addEventListener('submit', async (event) => { |
| event.preventDefault(); |
| const submitter = event.submitter; |
| const formData = new FormData(form); |
| if (submitter) { |
| formData.set(submitter.name, submitter.value); |
| } |
| const response = await fetch(form.action, { |
| method: 'POST', |
| headers: { 'X-Requested-With': 'fetch' }, |
| body: formData, |
| }); |
| if (!response.ok) { |
| refreshFeed(); |
| return; |
| } |
| refreshFeed(); |
| }); |
| }); |
| }; |
| |
| const scheduleFeedRefresh = (delay = FEED_POLL_INTERVAL_MS) => { |
| if (feedTimerId) { |
| window.clearTimeout(feedTimerId); |
| } |
| feedTimerId = window.setTimeout(runFeedRefresh, delay); |
| }; |
| |
| const runFeedRefresh = async () => { |
| if (document.hidden || feedInFlight) { |
| scheduleFeedRefresh(document.hidden ? FEED_POLL_INTERVAL_MS : 1000); |
| return; |
| } |
| |
| feedInFlight = true; |
| try { |
| await refreshFeed(); |
| } finally { |
| feedInFlight = false; |
| scheduleFeedRefresh(FEED_POLL_INTERVAL_MS + Math.floor(Math.random() * 800)); |
| } |
| }; |
| |
| const refreshFeed = async () => { |
| try { |
| const search = activityFilter ? `?activity_id=${activityFilter}` : ''; |
| const response = await fetch(`/api/admin/reviews/feed${search}`, { headers: { 'X-Requested-With': 'fetch' } }); |
| if (!response.ok) return; |
| const payload = await response.json(); |
| |
| if (onlineAdminPill) { |
| onlineAdminPill.textContent = `当前在线管理员 ${payload.online_admin_count || 0}`; |
| } |
| |
| if (payload.assigned_submissions && payload.assigned_submissions.length) { |
| assignedGrid.innerHTML = payload.assigned_submissions.map(cardHtml).join(''); |
| } else { |
| assignedGrid.innerHTML = ` |
| <article class="empty-state" id="assigned-empty-state"> |
| <h3>当前还没有分配给你的待审核内容</h3> |
| <p>保持页面打开,系统会自动把新的待审核任务分配给在线管理员。</p> |
| </article>`; |
| } |
| |
| if (payload.recent_submissions && payload.recent_submissions.length) { |
| recentList.innerHTML = payload.recent_submissions.map(recentHtml).join(''); |
| } else { |
| recentList.innerHTML = '<p class="muted">还没有最近审核记录。</p>'; |
| } |
| attachReviewForms(); |
| } catch (error) { |
| console.debug('review feed refresh skipped', error); |
| } |
| }; |
| |
| attachReviewForms(); |
| runFeedRefresh(); |
| document.addEventListener('visibilitychange', () => { |
| if (document.hidden) { |
| if (feedTimerId) { |
| window.clearTimeout(feedTimerId); |
| feedTimerId = null; |
| } |
| return; |
| } |
| runFeedRefresh(); |
| }); |
| window.addEventListener('focus', runFeedRefresh); |
| window.addEventListener('online', runFeedRefresh); |
| })(); |
| </script> |
| {% endblock %}
|
|
|
|
|