创赢未来评分系统 - 初始化提交(移除大文件)
All checks were successful
Deploy to Server / deploy (push) Successful in 18s
All checks were successful
Deploy to Server / deploy (push) Successful in 18s
This commit is contained in:
823
backend/competition/templates/judge/dashboard.html
Normal file
823
backend/competition/templates/judge/dashboard.html
Normal file
@@ -0,0 +1,823 @@
|
||||
{% extends 'judge/base.html' %}
|
||||
|
||||
{% block title %}项目列表 - 评委系统{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.markdown-body p { margin-bottom: 0.5em; }
|
||||
.markdown-body ul { list-style-type: disc; padding-left: 1.5em; margin-bottom: 0.5em; }
|
||||
.markdown-body ol { list-style-type: decimal; padding-left: 1.5em; margin-bottom: 0.5em; }
|
||||
.markdown-body strong { font-weight: 600; }
|
||||
.markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4 { font-weight: 600; margin-top: 1em; margin-bottom: 0.5em; }
|
||||
.markdown-body { overflow: hidden; }
|
||||
.line-clamp-5 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 5;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
{% 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>
|
||||
<h4 class="text-lg font-semibold text-gray-900 mb-3 flex items-center"><i class="fas fa-headphones mr-2 text-blue-500"></i>项目录音</h4>
|
||||
<div id="modalAudioSection" class="bg-gray-50 p-4 rounded-t-lg border border-gray-100 flex items-center justify-center min-h-[80px]">
|
||||
<!-- Audio player or "No audio" message will be injected here -->
|
||||
</div>
|
||||
<div id="subtitleContainer" class="bg-black text-white p-3 rounded-b-lg text-center min-h-[48px] flex items-center justify-center text-lg font-medium" style="display: none;">
|
||||
<span id="subtitleText"></span>
|
||||
</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>
|
||||
<div class="relative">
|
||||
<div class="text-sm text-gray-800 markdown-body line-clamp-5 transition-all duration-300" id="modalAiSummary"></div>
|
||||
<button id="toggleAiSummaryBtn" type="button" onclick="toggleAiSummary()" class="text-xs text-blue-600 hover:text-blue-800 focus:outline-none flex items-center mt-2 hidden">
|
||||
<i class="fas fa-chevron-down mr-1" id="toggleAiSummaryIcon"></i> <span id="toggleAiSummaryText">点击完整显示</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-gray-100 pt-3 relative">
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<p class="text-xs font-semibold text-gray-500 uppercase">逐字稿片段</p>
|
||||
<button type="button" onclick="openFullTranscriptionModal()" class="text-xs text-blue-600 hover:text-blue-800 focus:outline-none flex items-center">
|
||||
<i class="fas fa-expand-arrows-alt mr-1"></i> 查看完整逐字稿与章节
|
||||
</button>
|
||||
</div>
|
||||
<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 id="totalScoreDisplay" class="mt-4 p-3 bg-blue-50 rounded-lg border border-blue-200">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm font-medium text-blue-800">综合得分</span>
|
||||
<span id="totalScoreValue" class="text-2xl font-bold text-blue-600">0</span>
|
||||
</div>
|
||||
<p class="text-xs text-blue-500 mt-1">各维度分数×权重相加,提交后计算所有评委平均值</p>
|
||||
</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">选择上传方式</label>
|
||||
<div class="flex space-x-4 mb-3">
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" name="uploadType" value="file" checked class="form-radio text-blue-600" onchange="toggleUploadType()">
|
||||
<span class="ml-2 text-sm text-gray-700">文件上传</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" name="uploadType" value="url" class="form-radio text-blue-600" onchange="toggleUploadType()">
|
||||
<span class="ml-2 text-sm text-gray-700">URL 上传</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件上传 -->
|
||||
<div id="fileUploadSection">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">选择文件 (支持mp3/mp4, ≤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="audio/mpeg,audio/mp4,audio/*,.mp3,.mp4" 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>
|
||||
|
||||
<!-- URL 上传 -->
|
||||
<div id="urlUploadSection" style="display: none;">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">音频 URL 地址</label>
|
||||
<input type="url" id="audioUrlInput" 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="https://example.com/audio.mp3">
|
||||
<p class="text-xs text-gray-500 mt-1">支持 MP3、MP4 等音频/视频格式的直链</p>
|
||||
</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>
|
||||
|
||||
<!-- Full Transcription Modal -->
|
||||
<div id="fullTranscriptionModal" class="modal fixed inset-0 z-[60] flex items-center justify-center p-4 sm:p-6" style="background-color: rgba(0,0,0,0.6);">
|
||||
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-4xl max-h-[90vh] 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('fullTranscriptionModal')">
|
||||
<i class="fas fa-times text-xl"></i>
|
||||
</button>
|
||||
|
||||
<div class="px-8 py-5 border-b border-gray-100 bg-gray-50 rounded-t-2xl flex justify-between items-center">
|
||||
<h2 class="text-xl font-bold text-gray-900 flex items-center"><i class="fas fa-file-alt text-blue-500 mr-2"></i>完整逐字稿与章节</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-8">
|
||||
<div class="space-y-8">
|
||||
<!-- Chapters Section -->
|
||||
<div id="chaptersSection" style="display:none;">
|
||||
<h3 class="text-lg font-bold text-gray-800 mb-4 flex items-center border-b pb-2"><i class="fas fa-list-ul mr-2 text-indigo-500"></i>章节内容</h3>
|
||||
<div id="modalChaptersContent" class="space-y-4">
|
||||
<!-- Chapters rendered here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Full Transcription Section -->
|
||||
<div>
|
||||
<h3 class="text-lg font-bold text-gray-800 mb-4 flex items-center border-b pb-2"><i class="fas fa-align-left mr-2 text-green-500"></i>完整逐字稿</h3>
|
||||
<div id="modalFullTrans" class="text-gray-700 text-sm leading-relaxed bg-gray-50 p-6 rounded-xl border border-gray-100 whitespace-pre-wrap"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="https://cdn.staticfile.net/marked/11.1.1/marked.min.js"></script>
|
||||
<script>
|
||||
/**
|
||||
* 更新单个维度分数显示并计算总分
|
||||
*/
|
||||
function updateDimensionScore(dimensionId, maxScore, weight, value) {
|
||||
// 更新分数显示
|
||||
document.getElementById('score_val_' + dimensionId).innerText = value;
|
||||
// 重新计算总分
|
||||
calculateTotalScore();
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算评委的综合得分
|
||||
* 公式:直接用原始分数乘以权重相加 (与后端逻辑一致)
|
||||
*/
|
||||
function calculateTotalScore() {
|
||||
const dimensionsContainer = document.getElementById('scoreDimensions');
|
||||
const dimensionDivs = dimensionsContainer.querySelectorAll('[data-dimension-id]');
|
||||
|
||||
let totalScore = 0;
|
||||
|
||||
dimensionDivs.forEach(div => {
|
||||
const weight = parseFloat(div.dataset.weight);
|
||||
const dimensionId = div.dataset.dimensionId;
|
||||
|
||||
// 获取当前分数
|
||||
const scoreInput = document.querySelector('input[name="score_' + dimensionId + '"]');
|
||||
if (scoreInput) {
|
||||
const score = parseFloat(scoreInput.value) || 0;
|
||||
// 直接用原始分数乘以权重相加
|
||||
totalScore += score * weight;
|
||||
}
|
||||
});
|
||||
|
||||
// 更新显示
|
||||
const totalScoreElement = document.getElementById('totalScoreValue');
|
||||
if (totalScoreElement) {
|
||||
totalScoreElement.innerText = totalScore.toFixed(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换 AI 总结内容的显示状态(折叠/展开)
|
||||
* 通过添加或移除 line-clamp-5 类来实现截断或完整显示,并更新按钮的文字和图标。
|
||||
*/
|
||||
function toggleAiSummary() {
|
||||
const summaryDiv = document.getElementById('modalAiSummary');
|
||||
const toggleText = document.getElementById('toggleAiSummaryText');
|
||||
const toggleIcon = document.getElementById('toggleAiSummaryIcon');
|
||||
|
||||
if (summaryDiv.classList.contains('line-clamp-5')) {
|
||||
summaryDiv.classList.remove('line-clamp-5');
|
||||
toggleText.innerText = '收起内容';
|
||||
toggleIcon.className = 'fas fa-chevron-up mr-1';
|
||||
} else {
|
||||
summaryDiv.classList.add('line-clamp-5');
|
||||
toggleText.innerText = '点击完整显示';
|
||||
toggleIcon.className = 'fas fa-chevron-down mr-1';
|
||||
}
|
||||
}
|
||||
|
||||
function updateFileName(input) {
|
||||
const display = document.getElementById('fileNameDisplay');
|
||||
if (input.files.length > 0) {
|
||||
display.innerText = `已选: ${input.files.length} 个文件`;
|
||||
} else {
|
||||
display.innerText = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换文件上传和URL上传的显示
|
||||
* 根据用户选择显示对应的输入区域
|
||||
*/
|
||||
function toggleUploadType() {
|
||||
const uploadType = document.querySelector('input[name="uploadType"]:checked').value;
|
||||
const fileUploadSection = document.getElementById('fileUploadSection');
|
||||
const urlUploadSection = document.getElementById('urlUploadSection');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const urlInput = document.getElementById('audioUrlInput');
|
||||
|
||||
if (uploadType === 'file') {
|
||||
fileUploadSection.style.display = 'block';
|
||||
urlUploadSection.style.display = 'none';
|
||||
fileInput.required = true;
|
||||
urlInput.required = false;
|
||||
} else {
|
||||
fileUploadSection.style.display = 'none';
|
||||
urlUploadSection.style.display = 'block';
|
||||
fileInput.required = false;
|
||||
urlInput.required = true;
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal(id) {
|
||||
const modal = document.getElementById(id);
|
||||
modal.classList.remove('active');
|
||||
|
||||
// Stop audio if it's playing when modal is closed
|
||||
const audios = modal.querySelectorAll('audio');
|
||||
audios.forEach(audio => {
|
||||
audio.pause();
|
||||
});
|
||||
}
|
||||
|
||||
function openUploadModal() {
|
||||
document.getElementById('uploadModal').classList.add('active');
|
||||
}
|
||||
|
||||
function openFullTranscriptionModal() {
|
||||
if (!window.currentAiData) return;
|
||||
|
||||
document.getElementById('modalFullTrans').innerText = window.currentAiData.transcription || '暂无完整逐字稿';
|
||||
|
||||
let chaptersData = window.currentAiData.auto_chapters_data;
|
||||
const chaptersSection = document.getElementById('chaptersSection');
|
||||
const chaptersContent = document.getElementById('modalChaptersContent');
|
||||
|
||||
// Check if chaptersData is a string and parse it if necessary
|
||||
if (typeof chaptersData === 'string') {
|
||||
try {
|
||||
chaptersData = JSON.parse(chaptersData);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse auto_chapters_data:', e);
|
||||
chaptersData = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (chaptersData && chaptersData.AutoChapters && chaptersData.AutoChapters.length > 0) {
|
||||
chaptersSection.style.display = 'block';
|
||||
chaptersContent.innerHTML = chaptersData.AutoChapters.map(chapter => {
|
||||
// Convert ms to mm:ss format
|
||||
const formatTime = ms => {
|
||||
const totalSeconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const start = formatTime(chapter.Start);
|
||||
const end = formatTime(chapter.End);
|
||||
|
||||
return `
|
||||
<div class="bg-white p-4 rounded-lg border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h4 class="font-bold text-gray-800 text-sm">${chapter.Headline || '未命名章节'}</h4>
|
||||
<span class="text-xs font-mono text-indigo-600 bg-indigo-50 px-2 py-1 rounded">${start} - ${end}</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600">${chapter.Summary || '无摘要'}</p>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
} else {
|
||||
chaptersSection.style.display = 'none';
|
||||
}
|
||||
|
||||
document.getElementById('fullTranscriptionModal').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>';
|
||||
|
||||
// Render Audio Player
|
||||
const audioSection = document.getElementById('modalAudioSection');
|
||||
const subtitleContainer = document.getElementById('subtitleContainer');
|
||||
const subtitleText = document.getElementById('subtitleText');
|
||||
|
||||
if (subtitleContainer && subtitleText) {
|
||||
subtitleText.innerText = '';
|
||||
subtitleContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
if (data.audio_url) {
|
||||
audioSection.innerHTML = `
|
||||
<audio id="projectAudio" controls class="w-full">
|
||||
<source src="${data.audio_url}" type="audio/mpeg">
|
||||
<source src="${data.audio_url}" type="audio/mp4">
|
||||
您的浏览器不支持音频播放。
|
||||
</audio>
|
||||
`;
|
||||
audioSection.classList.remove('justify-center');
|
||||
} else {
|
||||
audioSection.innerHTML = `
|
||||
<div class="text-center text-gray-400 py-2">
|
||||
<i class="fas fa-microphone-slash text-2xl mb-2 block"></i>
|
||||
<span class="text-sm">暂未上传录音</span>
|
||||
</div>
|
||||
`;
|
||||
audioSection.classList.add('justify-center');
|
||||
}
|
||||
|
||||
// AI Result
|
||||
const aiSection = document.getElementById('aiResultSection');
|
||||
if (data.ai_result) {
|
||||
aiSection.style.display = 'block';
|
||||
|
||||
// Subtitle integration
|
||||
if (data.audio_url && data.ai_result.transcription_data && subtitleContainer && subtitleText) {
|
||||
let transData = data.ai_result.transcription_data;
|
||||
if (typeof transData === 'string') {
|
||||
try { transData = JSON.parse(transData); } catch(e) { console.error('Error parsing transcription_data', e); }
|
||||
}
|
||||
|
||||
if (transData && transData.Transcription && transData.Transcription.Paragraphs) {
|
||||
subtitleContainer.style.display = 'flex';
|
||||
|
||||
const sentences = [];
|
||||
transData.Transcription.Paragraphs.forEach(p => {
|
||||
if (p.Words && p.Words.length > 0) {
|
||||
let currentSentenceId = null;
|
||||
let currentSentence = { text: '', start: 0, end: 0 };
|
||||
|
||||
p.Words.forEach(w => {
|
||||
if (w.SentenceId !== currentSentenceId) {
|
||||
if (currentSentenceId !== null) {
|
||||
sentences.push({...currentSentence});
|
||||
}
|
||||
currentSentenceId = w.SentenceId;
|
||||
currentSentence = { text: w.Text, start: w.Start, end: w.End };
|
||||
} else {
|
||||
currentSentence.text += w.Text;
|
||||
currentSentence.end = w.End;
|
||||
}
|
||||
});
|
||||
if (currentSentenceId !== null) {
|
||||
sentences.push({...currentSentence});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const audio = document.getElementById('projectAudio');
|
||||
if (audio) {
|
||||
audio.addEventListener('timeupdate', () => {
|
||||
const currentTimeMs = audio.currentTime * 1000;
|
||||
// Add a small buffer (e.g. 200ms) to make subtitle display smoother
|
||||
const activeSentence = sentences.find(s => currentTimeMs >= (s.start - 200) && currentTimeMs <= (s.end + 200));
|
||||
if (activeSentence) {
|
||||
subtitleText.innerText = activeSentence.text;
|
||||
} else {
|
||||
subtitleText.innerText = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const summaryDiv = document.getElementById('modalAiSummary');
|
||||
const toggleBtn = document.getElementById('toggleAiSummaryBtn');
|
||||
const toggleText = document.getElementById('toggleAiSummaryText');
|
||||
const toggleIcon = document.getElementById('toggleAiSummaryIcon');
|
||||
|
||||
const summaryText = data.ai_result.summary || '暂无总结';
|
||||
summaryDiv.innerHTML = marked.parse(summaryText);
|
||||
|
||||
// Reset to collapsed state
|
||||
summaryDiv.classList.add('line-clamp-5');
|
||||
toggleText.innerText = '点击完整显示';
|
||||
toggleIcon.className = 'fas fa-chevron-down mr-1';
|
||||
|
||||
// Show/hide toggle button based on content height
|
||||
setTimeout(() => {
|
||||
if (summaryDiv.scrollHeight > summaryDiv.clientHeight) {
|
||||
toggleBtn.classList.remove('hidden');
|
||||
} else {
|
||||
toggleBtn.classList.add('hidden');
|
||||
}
|
||||
}, 50);
|
||||
|
||||
document.getElementById('modalAiTrans').innerText = (data.ai_result.transcription || '暂无内容').substring(0, 150) + '...';
|
||||
|
||||
// Store full data for full transcription modal
|
||||
window.currentAiData = data.ai_result;
|
||||
} else {
|
||||
aiSection.style.display = 'none';
|
||||
window.currentAiData = null;
|
||||
}
|
||||
|
||||
// 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" data-dimension-id="${d.id}" data-max-score="${d.max_score}" data-weight="${d.weight}">
|
||||
<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.max_score} | 权重: ${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="updateDimensionScore('${d.id}', '${d.max_score}', '${d.weight}', 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;
|
||||
|
||||
// 计算初始总分
|
||||
calculateTotalScore();
|
||||
|
||||
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 audioUrlInput = document.getElementById('audioUrlInput');
|
||||
const projectId = projectSelect.value;
|
||||
const files = fileInput.files;
|
||||
const uploadType = document.querySelector('input[name="uploadType"]:checked').value;
|
||||
|
||||
const progressBar = document.getElementById('uploadProgressBar');
|
||||
const statusText = document.getElementById('uploadStatusText');
|
||||
const percentText = document.getElementById('uploadPercent');
|
||||
const container = document.getElementById('uploadProgressContainer');
|
||||
|
||||
container.style.display = 'block';
|
||||
|
||||
if (uploadType === 'url') {
|
||||
// URL 上传模式
|
||||
const audioUrl = audioUrlInput.value.trim();
|
||||
if (!audioUrl) {
|
||||
alert('请输入音频 URL');
|
||||
container.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
statusText.innerText = '正在处理 URL...';
|
||||
progressBar.style.width = '30%';
|
||||
percentText.innerText = '30%';
|
||||
|
||||
try {
|
||||
const res = await fetch('/judge/api/upload/url/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token }}'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: audioUrl,
|
||||
project_id: projectId
|
||||
})
|
||||
});
|
||||
const result = await res.json();
|
||||
|
||||
if (result.success) {
|
||||
progressBar.style.width = '100%';
|
||||
percentText.innerText = '100%';
|
||||
statusText.innerText = '上传成功!';
|
||||
|
||||
setTimeout(() => {
|
||||
closeModal('uploadModal');
|
||||
container.style.display = 'none';
|
||||
window.location.href = "{% url 'judge_ai_manage' %}";
|
||||
}, 1000);
|
||||
} else {
|
||||
alert('上传失败: ' + result.message);
|
||||
container.style.display = 'none';
|
||||
}
|
||||
} catch (err) {
|
||||
alert('上传出错: ' + err);
|
||||
container.style.display = 'none';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 文件上传模式 (原有用 XMLHttpRequest)
|
||||
if (files.length === 0) return;
|
||||
|
||||
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 %}
|
||||
Reference in New Issue
Block a user