cam / app /templates /admin_reviews.html
cacode's picture
Upload 67 files
cf289c1 verified
{% 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>&nbsp;</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 %}