Files
market_page/backend/competition/templates/judge/dashboard.html
jeremygan2021 1f693e0e8a
All checks were successful
Deploy to Server / deploy (push) Successful in 16s
打分上传后台
2026-03-12 13:34:47 +08:00

425 lines
22 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends 'judge/base.html' %}
{% block title %}项目列表 - 评委系统{% endblock %}
{% block content %}
<div class="flex flex-col sm:flex-row justify-between items-center mb-8 gap-4">
<div>
<h2 class="text-3xl font-bold text-gray-900 tracking-tight">参赛项目列表</h2>
<p class="mt-1 text-sm text-gray-500">请对以下分配给您的项目进行评审</p>
</div>
{% if request.session.judge_role == 'judge' or request.session.judge_role == 'guest' %}
<button class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all transform hover:scale-105" onclick="openUploadModal()">
<i class="fas fa-cloud-upload-alt mr-2"></i>批量上传音频
</button>
{% endif %}
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
{% for project in projects %}
<div class="bg-white rounded-xl shadow-md hover:shadow-xl transition-all duration-300 overflow-hidden group flex flex-col h-full border border-gray-100" data-id="{{ project.id }}">
<div class="relative overflow-hidden h-48">
{% if project.cover_image_url %}
<img src="{{ project.cover_image_url }}" alt="{{ project.title }}" class="w-full h-full object-cover transform group-hover:scale-110 transition-transform duration-500">
{% else %}
<div class="w-full h-full bg-gray-100 flex flex-col items-center justify-center text-gray-400">
<i class="fas fa-image text-4xl mb-2"></i>
<span class="text-sm">暂无封面</span>
</div>
{% endif %}
<div class="absolute top-2 right-2">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ project.status_class }} shadow-sm bg-opacity-90 backdrop-filter backdrop-blur-sm">
{{ project.get_status_display }}
</span>
</div>
</div>
<div class="p-6 flex-1 flex flex-col">
<h3 class="text-xl font-bold text-gray-900 mb-2 line-clamp-1" title="{{ project.title }}">{{ project.title }}</h3>
<div class="flex items-center text-sm text-gray-500 mb-4">
<i class="fas fa-user-circle mr-2 text-gray-400"></i>
<span>{{ project.contestant_name }}</span>
</div>
<div class="mt-auto pt-4 border-t border-gray-100 flex items-center justify-between">
<div class="flex flex-col">
<span class="text-xs text-gray-400 uppercase tracking-wider font-semibold">当前得分</span>
<span class="text-lg font-bold text-blue-600 score-display">{{ project.current_score|default:"--" }}</span>
</div>
<button class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors" onclick="viewProject({{ project.id }})">
详情 & 评分 <i class="fas fa-arrow-right ml-2 text-xs"></i>
</button>
</div>
</div>
</div>
{% empty %}
<div class="col-span-full">
<div class="text-center py-16 bg-white rounded-xl shadow-sm border border-gray-100">
<div class="mx-auto h-24 w-24 text-gray-200">
<i class="fas fa-folder-open text-6xl"></i>
</div>
<h3 class="mt-4 text-lg font-medium text-gray-900">暂无项目</h3>
<p class="mt-1 text-sm text-gray-500">当前没有分配给您的参赛项目。</p>
</div>
</div>
{% endfor %}
</div>
<!-- Project Detail & Grading Modal -->
<div id="projectModal" class="modal fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6" style="background-color: rgba(0,0,0,0.5);">
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-5xl max-h-[90vh] overflow-hidden flex flex-col relative animate-fade-in">
<button class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 z-10 p-2 rounded-full hover:bg-gray-100 transition-colors" onclick="closeModal('projectModal')">
<i class="fas fa-times text-xl"></i>
</button>
<div class="px-8 py-6 border-b border-gray-100 bg-gray-50">
<h2 class="text-2xl font-bold text-gray-900" id="modalTitle">项目标题</h2>
<div class="flex items-center gap-4 mt-2 text-sm text-gray-500">
<span class="flex items-center"><i class="fas fa-hashtag mr-1"></i> <span id="modalId"></span></span>
<span class="flex items-center"><i class="fas fa-user mr-1"></i> <span id="modalContestant"></span></span>
</div>
</div>
<div class="flex-1 overflow-y-auto p-8">
<div class="flex flex-col lg:flex-row gap-8">
<!-- Left Column: Info -->
<div class="flex-1 space-y-6">
<div>
<h4 class="text-lg font-semibold text-gray-900 mb-3 flex items-center"><i class="fas fa-info-circle mr-2 text-blue-500"></i>项目简介</h4>
<div id="modalDesc" class="bg-gray-50 p-4 rounded-lg text-gray-700 text-sm leading-relaxed border border-gray-100 max-h-48 overflow-y-auto"></div>
</div>
<div id="aiResultSection" style="display:none;" class="border border-indigo-100 rounded-xl overflow-hidden">
<div class="bg-indigo-50 px-4 py-3 border-b border-indigo-100 flex items-center">
<i class="fas fa-robot text-indigo-600 mr-2"></i>
<h4 class="text-sm font-bold text-indigo-900 uppercase tracking-wide">AI 智能分析</h4>
</div>
<div class="p-4 bg-white space-y-4">
<div>
<p class="text-xs font-semibold text-gray-500 uppercase mb-1">AI 总结</p>
<p class="text-sm text-gray-800" id="modalAiSummary"></p>
</div>
<div class="border-t border-gray-100 pt-3">
<p class="text-xs font-semibold text-gray-500 uppercase mb-1">逐字稿片段</p>
<p class="text-sm text-gray-600 italic" id="modalAiTrans"></p>
</div>
</div>
</div>
<div>
<h4 class="text-lg font-semibold text-gray-900 mb-3 flex items-center"><i class="fas fa-history mr-2 text-blue-500"></i>历史评语</h4>
<div id="modalHistoryComments" class="space-y-3 max-h-60 overflow-y-auto pr-2">
<!-- Loaded via JS -->
</div>
</div>
</div>
<!-- Right Column: Grading -->
<div class="lg:w-1/3 bg-gray-50 p-6 rounded-xl border border-gray-200 h-fit sticky top-0">
<h4 class="text-lg font-bold text-gray-900 mb-4 flex items-center"><i class="fas fa-star mr-2 text-yellow-500"></i>打分 & 评语</h4>
<form id="gradingForm" onsubmit="submitScore(event)" class="space-y-6">
<input type="hidden" id="projectId" name="project_id">
<div id="scoreDimensions" class="space-y-4">
<!-- Dimensions loaded via JS -->
</div>
<div class="space-y-2">
<label for="comment" class="block text-sm font-medium text-gray-700">评语建议</label>
<textarea id="comment" name="comment" rows="4"
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md p-3"
placeholder="请输入您的专业点评..."></textarea>
</div>
<div class="pt-4 border-t border-gray-200 flex items-center justify-between">
<span id="saveStatus" class="text-green-600 text-sm font-medium opacity-0 transition-opacity duration-300 flex items-center">
<i class="fas fa-check mr-1"></i> 已保存
</span>
<button type="submit" class="inline-flex justify-center py-2 px-6 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors">
提交评分
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- Upload Modal -->
<div id="uploadModal" class="modal fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: rgba(0,0,0,0.5);">
<div class="bg-white rounded-lg shadow-xl w-full max-w-md overflow-hidden animate-fade-in relative">
<button class="absolute top-3 right-3 text-gray-400 hover:text-gray-600" onclick="closeModal('uploadModal')">
<i class="fas fa-times"></i>
</button>
<div class="px-6 py-4 bg-gray-50 border-b border-gray-100">
<h2 class="text-lg font-bold text-gray-900">上传项目音频</h2>
</div>
<div class="p-6">
<form id="uploadForm" onsubmit="uploadFiles(event)" class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">选择项目</label>
<select id="uploadProjectSelect" required class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md">
{% for project in projects %}
<option value="{{ project.id }}">{{ project.title }}</option>
{% endfor %}
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">选择文件 (支持mp3/mp4, &le;50MB)</label>
<div class="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md hover:border-blue-400 transition-colors cursor-pointer" onclick="document.getElementById('fileInput').click()">
<div class="space-y-1 text-center">
<i class="fas fa-cloud-upload-alt text-gray-400 text-3xl mb-2"></i>
<div class="flex text-sm text-gray-600">
<label for="fileInput" class="relative cursor-pointer bg-white rounded-md font-medium text-blue-600 hover:text-blue-500 focus-within:outline-none">
<span>点击上传</span>
<input id="fileInput" name="fileInput" type="file" class="sr-only" multiple accept=".mp3,.mp4" required onchange="updateFileName(this)">
</label>
<p class="pl-1">或拖拽文件到这里</p>
</div>
<p class="text-xs text-gray-500">MP3, MP4 up to 50MB</p>
<p id="fileNameDisplay" class="text-xs text-blue-600 mt-2 font-medium"></p>
</div>
</div>
</div>
<div id="uploadProgressContainer" style="display: none;" class="bg-gray-50 p-3 rounded-md">
<div class="flex justify-between text-xs text-gray-600 mb-1">
<span id="uploadStatusText">准备上传...</span>
<span id="uploadPercent">0%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div id="uploadProgressBar" class="bg-blue-600 h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
</div>
<button type="submit" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors">
开始上传
</button>
</form>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
function updateFileName(input) {
const display = document.getElementById('fileNameDisplay');
if (input.files.length > 0) {
display.innerText = `已选: ${input.files.length} 个文件`;
} else {
display.innerText = '';
}
}
function closeModal(id) {
document.getElementById(id).classList.remove('active');
}
function openUploadModal() {
document.getElementById('uploadModal').classList.add('active');
}
async function viewProject(id) {
try {
// Show loading state or skeleton if possible, for now just fetch
const data = await fetch(`/judge/api/projects/${id}/`).then(res => res.json());
document.getElementById('projectId').value = id;
document.getElementById('modalTitle').innerText = data.title;
document.getElementById('modalId').innerText = data.id;
document.getElementById('modalContestant').innerText = data.contestant_name;
document.getElementById('modalDesc').innerHTML = data.description || '<span class="text-gray-400 italic">暂无简介</span>';
// AI Result
const aiSection = document.getElementById('aiResultSection');
if (data.ai_result) {
aiSection.style.display = 'block';
document.getElementById('modalAiSummary').innerText = (data.ai_result.summary || '暂无总结').substring(0, 300) + (data.ai_result.summary && data.ai_result.summary.length > 300 ? '...' : '');
document.getElementById('modalAiTrans').innerText = (data.ai_result.transcription || '暂无内容').substring(0, 150) + '...';
} else {
aiSection.style.display = 'none';
}
// Render History Comments
const historyHtml = data.history_comments.length > 0 ? data.history_comments.map(c =>
`<div class="bg-white p-3 rounded border border-gray-100 shadow-sm">
<div class="flex justify-between items-center mb-1">
<span class="font-bold text-sm text-gray-800">${c.judge_name}</span>
<span class="text-xs text-gray-400">${c.created_at}</span>
</div>
<p class="text-sm text-gray-600">${c.content}</p>
</div>`
).join('') : '<div class="text-center text-gray-400 py-4 text-sm">暂无历史评语</div>';
document.getElementById('modalHistoryComments').innerHTML = historyHtml;
// Render Score Inputs
const dimensionsHtml = data.dimensions.map(d => `
<div class="bg-white p-3 rounded-lg border border-gray-200">
<div class="flex justify-between items-center mb-2">
<label class="text-sm font-medium text-gray-700">${d.name}</label>
<span class="text-xs text-gray-500 bg-gray-100 px-2 py-0.5 rounded">权重: ${d.weight}</span>
</div>
<div class="flex items-center gap-4">
<input type="range" min="0" max="${d.max_score}" step="1" value="${d.current_score || 0}"
oninput="document.getElementById('score_val_${d.id}').innerText = this.value"
name="score_${d.id}" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-600">
<div class="w-12 text-right">
<span id="score_val_${d.id}" class="text-lg font-bold text-blue-600">${d.current_score || 0}</span>
<span class="text-xs text-gray-400">/${d.max_score}</span>
</div>
</div>
</div>
`).join('');
document.getElementById('scoreDimensions').innerHTML = dimensionsHtml;
document.getElementById('comment').value = data.current_comment || '';
// Handle Grading Permission
const gradingForm = document.getElementById('gradingForm');
const gradingContainer = gradingForm.parentElement;
let readOnlyMsg = document.getElementById('readOnlyMsg');
if (!readOnlyMsg) {
readOnlyMsg = document.createElement('div');
readOnlyMsg.id = 'readOnlyMsg';
readOnlyMsg.className = 'text-center py-10 bg-white rounded-lg border border-dashed border-gray-300 mt-4';
readOnlyMsg.innerHTML = `
<div class="mx-auto h-12 w-12 bg-gray-50 rounded-full flex items-center justify-center mb-3">
<i class="fas fa-eye text-2xl text-gray-400"></i>
</div>
<h3 class="text-base font-medium text-gray-900">仅浏览模式</h3>
<p class="mt-1 text-sm text-gray-500">当前身份无法进行评分</p>
`;
gradingContainer.appendChild(readOnlyMsg);
}
if (data.can_grade) {
gradingForm.style.display = 'block';
readOnlyMsg.style.display = 'none';
} else {
gradingForm.style.display = 'none';
readOnlyMsg.style.display = 'block';
}
document.getElementById('projectModal').classList.add('active');
} catch (e) {
console.error(e);
alert('加载项目详情失败');
}
}
async function submitScore(e) {
e.preventDefault();
if(!confirm('确认提交评分吗?')) return;
const form = e.target;
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
try {
const res = await fetch('/judge/api/score/submit/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
},
body: JSON.stringify(data)
});
const result = await res.json();
if(result.success) {
const status = document.getElementById('saveStatus');
status.style.opacity = '1';
setTimeout(() => status.style.opacity = '0', 2000);
// Optional: Update card score in background
const cardScore = document.querySelector(`.project-card[data-id="${data.project_id}"] .score-display`);
if(cardScore) cardScore.innerText = '已评分'; // Or fetch new score
} else {
alert('提交失败: ' + result.message);
}
} catch(e) {
alert('提交出错');
}
}
async function uploadFiles(e) {
e.preventDefault();
const projectSelect = document.getElementById('uploadProjectSelect');
const fileInput = document.getElementById('fileInput');
const projectId = projectSelect.value;
const files = fileInput.files;
if (files.length === 0) return;
const progressBar = document.getElementById('uploadProgressBar');
const statusText = document.getElementById('uploadStatusText');
const percentText = document.getElementById('uploadPercent');
const container = document.getElementById('uploadProgressContainer');
container.style.display = 'block';
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (file.size > 50 * 1024 * 1024) {
alert(`文件 ${file.name} 超过 50MB跳过`);
continue;
}
const formData = new FormData();
formData.append('file', file);
formData.append('project_id', projectId);
statusText.innerText = `正在上传 ${file.name} (${i+1}/${files.length})...`;
progressBar.style.width = '0%';
percentText.innerText = '0%';
try {
await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', '/judge/api/upload/', true);
xhr.setRequestHeader('X-CSRFToken', '{{ csrf_token }}');
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
const percentComplete = Math.round((e.loaded / e.total) * 100);
progressBar.style.width = percentComplete + '%';
percentText.innerText = percentComplete + '%';
}
};
xhr.onload = () => {
if (xhr.status === 200) {
const res = JSON.parse(xhr.responseText);
if(res.success) resolve();
else reject(res.message);
} else {
reject(xhr.statusText);
}
};
xhr.onerror = () => reject('Network Error');
xhr.send(formData);
});
} catch (err) {
alert(`上传 ${file.name} 失败: ${err}`);
}
}
statusText.innerText = '所有任务完成!';
progressBar.style.width = '100%';
percentText.innerText = '100%';
setTimeout(() => {
closeModal('uploadModal');
container.style.display = 'none';
window.location.href = "{% url 'judge_ai_manage' %}";
}, 1000);
}
</script>
{% endblock %}